kolega-code 0.1.0__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 (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,699 @@
1
+ import os
2
+ import tempfile
3
+ import pytest
4
+ from pathlib import Path
5
+ from unittest.mock import patch, mock_open
6
+ from datetime import datetime
7
+
8
+ from kolega_code.services.file_system import LocalFileSystem, FileSystemPath
9
+
10
+
11
+ class TestLocalFileSystem:
12
+ """Comprehensive tests for LocalFileSystem implementation."""
13
+
14
+ @pytest.fixture
15
+ def temp_dir(self):
16
+ """Create a temporary directory for testing."""
17
+ with tempfile.TemporaryDirectory() as temp_dir:
18
+ yield Path(temp_dir)
19
+
20
+ @pytest.fixture
21
+ def filesystem(self, temp_dir):
22
+ """Create a LocalFileSystem instance with a temporary root."""
23
+ return LocalFileSystem(root_path=temp_dir)
24
+
25
+ @pytest.fixture
26
+ def filesystem_no_root(self):
27
+ """Create a LocalFileSystem instance without a root path."""
28
+ return LocalFileSystem()
29
+
30
+ def test_init_with_root_path_string(self, temp_dir):
31
+ """Test initialization with root path as string."""
32
+ fs = LocalFileSystem(root_path=str(temp_dir))
33
+ assert fs.root_path == temp_dir
34
+
35
+ def test_init_with_root_path_pathlib(self, temp_dir):
36
+ """Test initialization with root path as Path object."""
37
+ fs = LocalFileSystem(root_path=temp_dir)
38
+ assert fs.root_path == temp_dir
39
+
40
+ def test_init_without_root_path(self):
41
+ """Test initialization without root path."""
42
+ fs = LocalFileSystem()
43
+ assert fs.root_path is None
44
+
45
+ def test_resolve_path_with_root(self, filesystem, temp_dir):
46
+ """Test path resolution with root path."""
47
+ resolved = filesystem._resolve_path("test.txt")
48
+ expected = temp_dir / "test.txt"
49
+ assert resolved == expected
50
+
51
+ def test_resolve_path_without_root(self, filesystem_no_root):
52
+ """Test path resolution without root path."""
53
+ resolved = filesystem_no_root._resolve_path("test.txt")
54
+ expected = Path("test.txt")
55
+ assert resolved == expected
56
+
57
+ def test_write_and_read_text(self, filesystem):
58
+ """Test writing and reading text files."""
59
+ content = "Hello, World!\nThis is a test file."
60
+ filesystem.write_text("test.txt", content)
61
+
62
+ read_content = filesystem.read_text("test.txt")
63
+ assert read_content == content
64
+
65
+ def test_write_and_read_text_with_encoding(self, filesystem):
66
+ """Test writing and reading text files with specific encoding."""
67
+ content = "Hello, 世界! 🌍"
68
+ filesystem.write_text("test_utf8.txt", content, encoding="utf-8")
69
+
70
+ read_content = filesystem.read_text("test_utf8.txt", encoding="utf-8")
71
+ assert read_content == content
72
+
73
+ def test_write_and_read_bytes(self, filesystem):
74
+ """Test writing and reading binary files."""
75
+ content = b"\x00\x01\x02\x03\xff\xfe\xfd"
76
+ filesystem.write_bytes("test.bin", content)
77
+
78
+ read_content = filesystem.read_bytes("test.bin")
79
+ assert read_content == content
80
+
81
+ def test_exists(self, filesystem):
82
+ """Test checking if files and directories exist."""
83
+ # File doesn't exist initially
84
+ assert not filesystem.exists("nonexistent.txt")
85
+
86
+ # Create file and check it exists
87
+ filesystem.write_text("exists_test.txt", "content")
88
+ assert filesystem.exists("exists_test.txt")
89
+
90
+ # Create directory and check it exists
91
+ filesystem.mkdir("test_dir")
92
+ assert filesystem.exists("test_dir")
93
+
94
+ def test_is_file(self, filesystem):
95
+ """Test checking if path is a file."""
96
+ # Create file
97
+ filesystem.write_text("test_file.txt", "content")
98
+ assert filesystem.is_file("test_file.txt")
99
+
100
+ # Create directory
101
+ filesystem.mkdir("test_dir")
102
+ assert not filesystem.is_file("test_dir")
103
+
104
+ # Non-existent path
105
+ assert not filesystem.is_file("nonexistent.txt")
106
+
107
+ def test_is_dir(self, filesystem):
108
+ """Test checking if path is a directory."""
109
+ # Create directory
110
+ filesystem.mkdir("test_dir")
111
+ assert filesystem.is_dir("test_dir")
112
+
113
+ # Create file
114
+ filesystem.write_text("test_file.txt", "content")
115
+ assert not filesystem.is_dir("test_file.txt")
116
+
117
+ # Non-existent path
118
+ assert not filesystem.is_dir("nonexistent_dir")
119
+
120
+ def test_stat(self, filesystem):
121
+ """Test getting file statistics."""
122
+ content = "Test content for stat"
123
+ filesystem.write_text("stat_test.txt", content)
124
+
125
+ stat_info = filesystem.stat("stat_test.txt")
126
+
127
+ assert "size" in stat_info
128
+ assert "modified_time" in stat_info
129
+ assert "created_time" in stat_info
130
+ assert "accessed_time" in stat_info
131
+ assert "is_directory" in stat_info
132
+ assert "is_file" in stat_info
133
+ assert "stat_result" in stat_info
134
+
135
+ assert stat_info["size"] == len(content.encode())
136
+ assert stat_info["is_file"] is True
137
+ assert stat_info["is_directory"] is False
138
+
139
+ def test_mkdir(self, filesystem):
140
+ """Test creating directories."""
141
+ # Simple directory creation
142
+ filesystem.mkdir("simple_dir")
143
+ assert filesystem.is_dir("simple_dir")
144
+
145
+ # Directory creation with parents
146
+ filesystem.mkdir("parent/child/grandchild", parents=True)
147
+ assert filesystem.is_dir("parent/child/grandchild")
148
+
149
+ # Directory creation with exist_ok
150
+ filesystem.mkdir("simple_dir", exist_ok=True) # Should not raise
151
+
152
+ # Directory creation without exist_ok should raise
153
+ with pytest.raises(FileExistsError):
154
+ filesystem.mkdir("simple_dir", exist_ok=False)
155
+
156
+ def test_remove(self, filesystem):
157
+ """Test removing files."""
158
+ # Create and remove file
159
+ filesystem.write_text("remove_test.txt", "content")
160
+ assert filesystem.exists("remove_test.txt")
161
+
162
+ filesystem.remove("remove_test.txt")
163
+ assert not filesystem.exists("remove_test.txt")
164
+
165
+ # Test missing_ok=True
166
+ filesystem.remove("nonexistent.txt", missing_ok=True) # Should not raise
167
+
168
+ # Test missing_ok=False (default)
169
+ with pytest.raises(FileNotFoundError):
170
+ filesystem.remove("nonexistent.txt", missing_ok=False)
171
+
172
+ def test_rmdir(self, filesystem):
173
+ """Test removing empty directories."""
174
+ # Create and remove empty directory
175
+ filesystem.mkdir("empty_dir")
176
+ assert filesystem.is_dir("empty_dir")
177
+
178
+ filesystem.rmdir("empty_dir")
179
+ assert not filesystem.exists("empty_dir")
180
+
181
+ # Test removing non-empty directory should raise
182
+ filesystem.mkdir("non_empty_dir")
183
+ filesystem.write_text("non_empty_dir/file.txt", "content")
184
+
185
+ with pytest.raises(OSError):
186
+ filesystem.rmdir("non_empty_dir")
187
+
188
+ def test_rmtree(self, filesystem):
189
+ """Test removing directories recursively."""
190
+ # Create directory structure
191
+ filesystem.mkdir("tree/branch1/leaf1", parents=True)
192
+ filesystem.mkdir("tree/branch2/leaf2", parents=True)
193
+ filesystem.write_text("tree/file.txt", "content")
194
+ filesystem.write_text("tree/branch1/file1.txt", "content1")
195
+
196
+ assert filesystem.is_dir("tree")
197
+
198
+ # Remove entire tree
199
+ filesystem.rmtree("tree")
200
+ assert not filesystem.exists("tree")
201
+
202
+ def test_listdir(self, filesystem):
203
+ """Test listing directory contents."""
204
+ # Create test structure
205
+ filesystem.mkdir("list_test")
206
+ filesystem.write_text("list_test/file1.txt", "content1")
207
+ filesystem.write_text("list_test/file2.txt", "content2")
208
+ filesystem.mkdir("list_test/subdir")
209
+
210
+ contents = filesystem.listdir("list_test")
211
+
212
+ assert len(contents) == 3
213
+ assert "file1.txt" in contents
214
+ assert "file2.txt" in contents
215
+ assert "subdir" in contents
216
+
217
+ def test_iterdir(self, filesystem):
218
+ """Test iterating over directory contents."""
219
+ # Create test structure
220
+ filesystem.mkdir("iter_test")
221
+ filesystem.write_text("iter_test/file1.txt", "content1")
222
+ filesystem.write_text("iter_test/file2.txt", "content2")
223
+ filesystem.mkdir("iter_test/subdir")
224
+
225
+ contents = list(filesystem.iterdir("iter_test"))
226
+
227
+ assert len(contents) == 3
228
+ assert "iter_test/file1.txt" in contents
229
+ assert "iter_test/file2.txt" in contents
230
+ assert "iter_test/subdir" in contents
231
+
232
+ def test_glob(self, filesystem):
233
+ """Test glob pattern matching."""
234
+ # Create test files
235
+ filesystem.write_text("test1.txt", "content")
236
+ filesystem.write_text("test2.txt", "content")
237
+ filesystem.write_text("test.py", "content")
238
+ filesystem.mkdir("subdir")
239
+ filesystem.write_text("subdir/test3.txt", "content")
240
+
241
+ # Test simple pattern
242
+ txt_files = filesystem.glob("*.txt")
243
+ assert "test1.txt" in txt_files
244
+ assert "test2.txt" in txt_files
245
+ assert "test.py" not in txt_files
246
+
247
+ # Test recursive pattern
248
+ all_txt = filesystem.glob("**/*.txt")
249
+ assert "test1.txt" in all_txt
250
+ assert "test2.txt" in all_txt
251
+ assert "subdir/test3.txt" in all_txt
252
+
253
+ def test_is_binary_file(self, filesystem):
254
+ """Test binary file detection."""
255
+ # Text file
256
+ filesystem.write_text("text.txt", "This is text content")
257
+ assert not filesystem.is_binary_file("text.txt")
258
+
259
+ # Binary file by extension
260
+ filesystem.write_bytes("binary.jpg", b"fake image data")
261
+ assert filesystem.is_binary_file("binary.jpg")
262
+
263
+ # Binary file by content (contains null bytes)
264
+ filesystem.write_bytes("binary_content.dat", b"text\x00with\x00nulls")
265
+ assert filesystem.is_binary_file("binary_content.dat")
266
+
267
+ def test_get_name(self, filesystem):
268
+ """Test getting filename from path."""
269
+ assert filesystem.get_name("test.txt") == "test.txt"
270
+ assert filesystem.get_name("dir/subdir/file.py") == "file.py"
271
+ assert filesystem.get_name("dir/") == "dir"
272
+ assert filesystem.get_name("dir") == "dir"
273
+
274
+ def test_get_suffix(self, filesystem):
275
+ """Test getting file extension."""
276
+ assert filesystem.get_suffix("test.txt") == ".txt"
277
+ assert filesystem.get_suffix("file.tar.gz") == ".gz"
278
+ assert filesystem.get_suffix("no_extension") == ""
279
+ assert filesystem.get_suffix("dir/file.py") == ".py"
280
+
281
+ def test_get_parent(self, filesystem):
282
+ """Test getting parent directory."""
283
+ # With root path, should return relative parent
284
+ parent = filesystem.get_parent("dir/subdir/file.txt")
285
+ assert parent == "dir/subdir"
286
+
287
+ parent = filesystem.get_parent("file.txt")
288
+ assert parent == "."
289
+
290
+ def test_get_parents(self, filesystem):
291
+ """Test getting all parent directories."""
292
+ parents = filesystem.get_parents("dir/subdir/file.txt")
293
+ assert "dir/subdir" in parents
294
+ assert "dir" in parents
295
+ assert "." in parents
296
+
297
+ def test_relative_to(self, filesystem):
298
+ """Test getting relative path."""
299
+ # Create some structure
300
+ filesystem.mkdir("base/sub", parents=True)
301
+
302
+ relative = filesystem.relative_to("base/sub/file.txt", "base")
303
+ assert relative == "sub/file.txt"
304
+
305
+ def test_join_path(self, filesystem):
306
+ """Test joining path components."""
307
+ joined = filesystem.join_path("dir", "subdir", "file.txt")
308
+ assert joined == "dir/subdir/file.txt"
309
+
310
+ def test_is_absolute(self, filesystem):
311
+ """Test checking if path is absolute."""
312
+ assert not filesystem.is_absolute("relative/path")
313
+ assert filesystem.is_absolute("/absolute/path")
314
+
315
+ # Windows absolute path
316
+ if os.name == "nt":
317
+ assert filesystem.is_absolute("C:\\absolute\\path")
318
+
319
+ def test_path_join(self, filesystem):
320
+ """Test os.path.join compatibility method."""
321
+ joined = filesystem.path_join("dir", "subdir", "file.txt")
322
+ expected = os.path.join("dir", "subdir", "file.txt")
323
+ assert joined == expected
324
+
325
+ def test_path_exists(self, filesystem):
326
+ """Test os.path.exists compatibility method."""
327
+ # This tests the raw os.path.exists, not the filesystem abstraction
328
+ with tempfile.NamedTemporaryFile() as tmp:
329
+ assert filesystem.path_exists(tmp.name)
330
+ assert not filesystem.path_exists("/nonexistent/path")
331
+
332
+ def test_path_isdir(self, filesystem):
333
+ """Test os.path.isdir compatibility method."""
334
+ with tempfile.TemporaryDirectory() as tmp_dir:
335
+ assert filesystem.path_isdir(tmp_dir)
336
+ assert not filesystem.path_isdir("/nonexistent/dir")
337
+
338
+ def test_path_isfile(self, filesystem):
339
+ """Test os.path.isfile compatibility method."""
340
+ with tempfile.NamedTemporaryFile() as tmp:
341
+ assert filesystem.path_isfile(tmp.name)
342
+ assert not filesystem.path_isfile("/nonexistent/file")
343
+
344
+ def test_format_datetime(self, filesystem):
345
+ """Test datetime formatting utility."""
346
+ timestamp = 1640995200.0 # 2022-01-01 00:00:00 UTC
347
+ formatted = filesystem.format_datetime(timestamp)
348
+ # The exact format depends on timezone, but should contain date components
349
+ assert "2022" in formatted or "2021" in formatted # Account for timezone differences
350
+ assert "-" in formatted
351
+ assert ":" in formatted
352
+
353
+ def test_open_context_manager(self, filesystem):
354
+ """Test file opening as context manager."""
355
+ content = "Test content for context manager"
356
+
357
+ # Write using context manager
358
+ with filesystem.open("context_test.txt", "w") as f:
359
+ f.write(content)
360
+
361
+ # Read using context manager
362
+ with filesystem.open("context_test.txt", "r") as f:
363
+ read_content = f.read()
364
+
365
+ assert read_content == content
366
+
367
+ def test_error_handling(self, filesystem):
368
+ """Test various error conditions."""
369
+ # FileNotFoundError for reading non-existent file
370
+ with pytest.raises(FileNotFoundError):
371
+ filesystem.read_text("nonexistent.txt")
372
+
373
+ # FileNotFoundError for stat on non-existent file
374
+ with pytest.raises(FileNotFoundError):
375
+ filesystem.stat("nonexistent.txt")
376
+
377
+ # NotADirectoryError for listing non-directory
378
+ filesystem.write_text("not_a_dir.txt", "content")
379
+ with pytest.raises(NotADirectoryError):
380
+ filesystem.listdir("not_a_dir.txt")
381
+
382
+
383
+ class TestFileSystemPath:
384
+ """Tests for the FileSystemPath wrapper class."""
385
+
386
+ @pytest.fixture
387
+ def temp_dir(self):
388
+ """Create a temporary directory for testing."""
389
+ with tempfile.TemporaryDirectory() as temp_dir:
390
+ yield Path(temp_dir)
391
+
392
+ @pytest.fixture
393
+ def filesystem(self, temp_dir):
394
+ """Create a LocalFileSystem instance with a temporary root."""
395
+ return LocalFileSystem(root_path=temp_dir)
396
+
397
+ @pytest.fixture
398
+ def fs_path(self, filesystem):
399
+ """Create a FileSystemPath instance."""
400
+ return filesystem.path("test_path")
401
+
402
+ def test_init_and_str(self, filesystem):
403
+ """Test FileSystemPath initialization and string representation."""
404
+ path = FileSystemPath(filesystem, "test/path")
405
+ assert str(path) == "test/path"
406
+ assert path.path == "test/path"
407
+
408
+ def test_truediv_operator(self, filesystem):
409
+ """Test path / 'subpath' syntax."""
410
+ path = FileSystemPath(filesystem, "base")
411
+ new_path = path / "subdir" / "file.txt"
412
+
413
+ assert str(new_path) == "base/subdir/file.txt"
414
+ assert isinstance(new_path, FileSystemPath)
415
+
416
+ def test_truediv_with_root_path(self, filesystem):
417
+ """Test path / 'subpath' syntax with root path."""
418
+ path = FileSystemPath(filesystem, ".")
419
+ new_path = path / "file.txt"
420
+
421
+ assert str(new_path) == "file.txt"
422
+
423
+ def test_name_property(self, filesystem):
424
+ """Test name property."""
425
+ path = FileSystemPath(filesystem, "dir/file.txt")
426
+ assert path.name == "file.txt"
427
+
428
+ def test_suffix_property(self, filesystem):
429
+ """Test suffix property."""
430
+ path = FileSystemPath(filesystem, "dir/file.txt")
431
+ assert path.suffix == ".txt"
432
+
433
+ def test_parent_property(self, filesystem):
434
+ """Test parent property."""
435
+ path = FileSystemPath(filesystem, "dir/subdir/file.txt")
436
+ parent = path.parent
437
+
438
+ assert isinstance(parent, FileSystemPath)
439
+ assert str(parent) == "dir/subdir"
440
+
441
+ def test_parents_property(self, filesystem):
442
+ """Test parents property."""
443
+ path = FileSystemPath(filesystem, "dir/subdir/file.txt")
444
+ parents = path.parents
445
+
446
+ assert all(isinstance(p, FileSystemPath) for p in parents)
447
+ parent_strs = [str(p) for p in parents]
448
+ assert "dir/subdir" in parent_strs
449
+ assert "dir" in parent_strs
450
+
451
+ def test_file_operations(self, filesystem):
452
+ """Test file operations through FileSystemPath."""
453
+ path = filesystem.path("test_file.txt")
454
+
455
+ # Write and read
456
+ path.write_text("Hello, World!")
457
+ content = path.read_text()
458
+ assert content == "Hello, World!"
459
+
460
+ # Check existence and type
461
+ assert path.exists()
462
+ assert path.is_file()
463
+ assert not path.is_dir()
464
+
465
+ # Get stats
466
+ stat_info = path.stat()
467
+ assert stat_info["is_file"] is True
468
+
469
+ def test_directory_operations(self, filesystem):
470
+ """Test directory operations through FileSystemPath."""
471
+ path = filesystem.path("test_dir")
472
+
473
+ # Create directory
474
+ path.mkdir()
475
+ assert path.exists()
476
+ assert path.is_dir()
477
+ assert not path.is_file()
478
+
479
+ # Create subdirectory with parents
480
+ subpath = path / "subdir" / "deep"
481
+ subpath.mkdir(parents=True)
482
+ assert subpath.exists()
483
+
484
+ def test_iterdir(self, filesystem):
485
+ """Test iterating over directory contents."""
486
+ # Create test structure
487
+ base_path = filesystem.path("iter_base")
488
+ base_path.mkdir()
489
+
490
+ (base_path / "file1.txt").write_text("content1")
491
+ (base_path / "file2.txt").write_text("content2")
492
+ (base_path / "subdir").mkdir()
493
+
494
+ # Iterate and collect
495
+ contents = list(base_path.iterdir())
496
+ content_names = [item.name for item in contents]
497
+
498
+ assert len(contents) == 3
499
+ assert "file1.txt" in content_names
500
+ assert "file2.txt" in content_names
501
+ assert "subdir" in content_names
502
+ assert all(isinstance(item, FileSystemPath) for item in contents)
503
+
504
+ def test_glob(self, filesystem):
505
+ """Test glob pattern matching through FileSystemPath."""
506
+ # Create test structure
507
+ base_path = filesystem.path("glob_base")
508
+ base_path.mkdir()
509
+
510
+ (base_path / "test1.txt").write_text("content")
511
+ (base_path / "test2.txt").write_text("content")
512
+ (base_path / "test.py").write_text("content")
513
+
514
+ # Test glob
515
+ txt_files = list(base_path.glob("*.txt"))
516
+ txt_names = [f.name for f in txt_files]
517
+
518
+ assert len(txt_files) == 2
519
+ assert "test1.txt" in txt_names
520
+ assert "test2.txt" in txt_names
521
+ assert all(isinstance(f, FileSystemPath) for f in txt_files)
522
+
523
+ def test_relative_to(self, filesystem):
524
+ """Test relative_to method."""
525
+ base_path = filesystem.path("base")
526
+ file_path = filesystem.path("base/sub/file.txt")
527
+
528
+ relative = file_path.relative_to(base_path)
529
+ assert relative == "sub/file.txt"
530
+
531
+ # Test with string
532
+ relative_str = file_path.relative_to("base")
533
+ assert relative_str == "sub/file.txt"
534
+
535
+ def test_unlink(self, filesystem):
536
+ """Test file removal through FileSystemPath."""
537
+ path = filesystem.path("remove_me.txt")
538
+ path.write_text("content")
539
+ assert path.exists()
540
+
541
+ path.unlink()
542
+ assert not path.exists()
543
+
544
+ # Test missing_ok
545
+ path.unlink(missing_ok=True) # Should not raise
546
+
547
+ def test_rmdir(self, filesystem):
548
+ """Test directory removal through FileSystemPath."""
549
+ path = filesystem.path("remove_dir")
550
+ path.mkdir()
551
+ assert path.exists()
552
+
553
+ path.rmdir()
554
+ assert not path.exists()
555
+
556
+ def test_binary_operations(self, filesystem):
557
+ """Test binary file operations through FileSystemPath."""
558
+ path = filesystem.path("binary_test.bin")
559
+ binary_data = b"\x00\x01\x02\x03\xff"
560
+
561
+ path.write_bytes(binary_data)
562
+ read_data = path.read_bytes()
563
+
564
+ assert read_data == binary_data
565
+
566
+ def test_open_context_manager(self, filesystem):
567
+ """Test opening files as context manager through FileSystemPath."""
568
+ path = filesystem.path("context_test.txt")
569
+ content = "Context manager test"
570
+
571
+ # Write using context manager
572
+ with path.open("w") as f:
573
+ f.write(content)
574
+
575
+ # Read using context manager
576
+ with path.open("r") as f:
577
+ read_content = f.read()
578
+
579
+ assert read_content == content
580
+
581
+
582
+ class TestFileSystemIntegration:
583
+ """Integration tests for filesystem operations."""
584
+
585
+ @pytest.fixture
586
+ def temp_dir(self):
587
+ """Create a temporary directory for testing."""
588
+ with tempfile.TemporaryDirectory() as temp_dir:
589
+ yield Path(temp_dir)
590
+
591
+ @pytest.fixture
592
+ def filesystem(self, temp_dir):
593
+ """Create a LocalFileSystem instance with a temporary root."""
594
+ return LocalFileSystem(root_path=temp_dir)
595
+
596
+ def test_complex_directory_structure(self, filesystem):
597
+ """Test creating and manipulating complex directory structures."""
598
+ # Create complex structure
599
+ structure = [
600
+ "project/src/main.py",
601
+ "project/src/utils/helper.py",
602
+ "project/tests/test_main.py",
603
+ "project/docs/readme.md",
604
+ "project/config.json",
605
+ ]
606
+
607
+ for file_path in structure:
608
+ path = filesystem.path(file_path)
609
+ path.parent.mkdir(parents=True, exist_ok=True)
610
+ path.write_text(f"Content of {file_path}")
611
+
612
+ # Verify structure
613
+ assert filesystem.exists("project")
614
+ assert filesystem.is_dir("project")
615
+ assert filesystem.exists("project/src/main.py")
616
+ assert filesystem.is_file("project/src/main.py")
617
+
618
+ # Test glob patterns
619
+ py_files = filesystem.glob("**/*.py")
620
+ assert len(py_files) == 3
621
+ assert "project/src/main.py" in py_files
622
+ assert "project/src/utils/helper.py" in py_files
623
+ assert "project/tests/test_main.py" in py_files
624
+
625
+ # Test directory listing
626
+ src_contents = list(filesystem.iterdir("project/src"))
627
+ assert "project/src/main.py" in src_contents
628
+ assert "project/src/utils" in src_contents
629
+
630
+ def test_file_operations_chain(self, filesystem):
631
+ """Test chaining multiple file operations."""
632
+ # Create initial file
633
+ path = filesystem.path("chain_test.txt")
634
+ path.write_text("Initial content")
635
+
636
+ # Read, modify, write back
637
+ content = path.read_text()
638
+ modified_content = content + "\nAdditional line"
639
+ path.write_text(modified_content)
640
+
641
+ # Verify modification
642
+ final_content = path.read_text()
643
+ assert "Initial content" in final_content
644
+ assert "Additional line" in final_content
645
+
646
+ # Get file stats
647
+ stats = path.stat()
648
+ assert stats["size"] > len("Initial content")
649
+
650
+ def test_error_recovery(self, filesystem):
651
+ """Test error handling and recovery scenarios."""
652
+ # Try to read non-existent file
653
+ with pytest.raises(FileNotFoundError):
654
+ filesystem.read_text("nonexistent.txt")
655
+
656
+ # Create file after error
657
+ filesystem.write_text("recovery_test.txt", "Recovered!")
658
+ content = filesystem.read_text("recovery_test.txt")
659
+ assert content == "Recovered!"
660
+
661
+ # Try to create directory that already exists
662
+ filesystem.mkdir("existing_dir")
663
+ filesystem.mkdir("existing_dir", exist_ok=True) # Should not raise
664
+
665
+ with pytest.raises(FileExistsError):
666
+ filesystem.mkdir("existing_dir", exist_ok=False)
667
+
668
+ def test_path_object_consistency(self, filesystem):
669
+ """Test that FileSystemPath objects maintain consistency."""
670
+ # Create path objects for same path
671
+ path1 = filesystem.path("consistency_test.txt")
672
+ path2 = filesystem.path("consistency_test.txt")
673
+
674
+ # Operations on one should be visible through the other
675
+ path1.write_text("Consistent content")
676
+ assert path2.exists()
677
+ assert path2.read_text() == "Consistent content"
678
+
679
+ # Path operations should work consistently
680
+ assert path1.name == path2.name
681
+ assert path1.suffix == path2.suffix
682
+ assert str(path1.parent) == str(path2.parent)
683
+
684
+ def test_mixed_operations(self, filesystem):
685
+ """Test mixing direct filesystem calls with FileSystemPath operations."""
686
+ # Create file using direct filesystem
687
+ filesystem.write_text("mixed_test.txt", "Direct content")
688
+
689
+ # Access using FileSystemPath
690
+ path = filesystem.path("mixed_test.txt")
691
+ assert path.exists()
692
+ assert path.read_text() == "Direct content"
693
+
694
+ # Modify using FileSystemPath
695
+ path.write_text("Modified via path")
696
+
697
+ # Read using direct filesystem
698
+ content = filesystem.read_text("mixed_test.txt")
699
+ assert content == "Modified via path"