spaceforge 0.1.0.dev0__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.
@@ -0,0 +1,621 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+ from typing import Any, Dict
7
+ from unittest.mock import Mock, patch
8
+
9
+ import pytest
10
+
11
+ from spaceforge.plugin import SpaceforgePlugin
12
+
13
+
14
+ class TestSpaceforgePlugin:
15
+ """Test the base SpaceforgePlugin class."""
16
+
17
+ def setup_method(self) -> None:
18
+ """Setup test fixtures."""
19
+ self.temp_dir = tempfile.mkdtemp()
20
+
21
+ def teardown_method(self) -> None:
22
+ """Cleanup test fixtures."""
23
+ import shutil
24
+
25
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
26
+
27
+ def test_spacepy_plugin_init_defaults(self) -> None:
28
+ """Test SpaceforgePlugin initialization with default environment."""
29
+ with patch.dict(os.environ, {}, clear=True):
30
+ plugin = SpaceforgePlugin()
31
+
32
+ assert plugin._api_token is False
33
+ assert plugin._spacelift_domain is False
34
+ assert plugin._api_enabled is False
35
+ assert plugin._workspace_root == os.getcwd()
36
+ assert isinstance(plugin.logger, logging.Logger)
37
+
38
+ def test_spacepy_plugin_init_with_api_credentials(self) -> None:
39
+ """Test SpaceforgePlugin initialization with API credentials."""
40
+ test_env = {
41
+ "SPACELIFT_API_TOKEN": "test_token",
42
+ "TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io",
43
+ "WORKSPACE_ROOT": "/test/workspace",
44
+ }
45
+
46
+ with patch.dict(os.environ, test_env, clear=True):
47
+ plugin = SpaceforgePlugin()
48
+
49
+ assert plugin._api_token == "test_token"
50
+ assert plugin._spacelift_domain == "https://test.spacelift.io"
51
+ assert plugin._api_enabled is True
52
+ assert plugin._workspace_root == "/test/workspace"
53
+
54
+ def test_spacepy_plugin_init_domain_trailing_slash(self) -> None:
55
+ """Test domain with trailing slash gets normalized."""
56
+ test_env = {
57
+ "SPACELIFT_API_TOKEN": "test_token",
58
+ "TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io/",
59
+ }
60
+
61
+ with patch.dict(os.environ, test_env, clear=True):
62
+ plugin = SpaceforgePlugin()
63
+
64
+ assert plugin._spacelift_domain == "https://test.spacelift.io"
65
+ assert plugin._api_enabled is True
66
+
67
+ def test_spacepy_plugin_init_domain_no_https(self) -> None:
68
+ """Test domain without https:// prefix disables API."""
69
+ test_env = {
70
+ "SPACELIFT_API_TOKEN": "test_token",
71
+ "TF_VAR_spacelift_graphql_endpoint": "test.spacelift.io",
72
+ }
73
+
74
+ with patch.dict(os.environ, test_env, clear=True):
75
+ plugin = SpaceforgePlugin()
76
+
77
+ assert plugin._spacelift_domain == "test.spacelift.io"
78
+ assert plugin._api_enabled is False
79
+
80
+ def test_spacepy_plugin_init_partial_credentials(self) -> None:
81
+ """Test initialization with only token or only domain."""
82
+ # Only token, no domain
83
+ with patch.dict(os.environ, {"SPACELIFT_API_TOKEN": "test_token"}, clear=True):
84
+ plugin = SpaceforgePlugin()
85
+ assert plugin._api_enabled is False
86
+
87
+ # Only domain, no token
88
+ with patch.dict(
89
+ os.environ,
90
+ {"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io"},
91
+ clear=True,
92
+ ):
93
+ plugin = SpaceforgePlugin()
94
+ assert plugin._api_enabled is False
95
+
96
+
97
+ class TestSpaceforgePluginLogging:
98
+ """Test the logging functionality."""
99
+
100
+ def test_logger_setup_basic(self) -> None:
101
+ """Test basic logger setup."""
102
+ with patch.dict(os.environ, {}, clear=True):
103
+ plugin = SpaceforgePlugin()
104
+
105
+ assert plugin.logger.name == "spaceforge.SpaceforgePlugin"
106
+ assert len(plugin.logger.handlers) >= 1
107
+ # Logger level might be DEBUG from previous tests, check effective level
108
+ assert plugin.logger.getEffectiveLevel() <= logging.INFO
109
+
110
+ def test_logger_setup_debug_mode(self) -> None:
111
+ """Test logger setup with debug mode enabled."""
112
+ test_env = {"SPACELIFT_DEBUG": "true"}
113
+
114
+ with patch.dict(os.environ, test_env, clear=True):
115
+ plugin = SpaceforgePlugin()
116
+
117
+ assert plugin.logger.level == logging.DEBUG
118
+
119
+ def test_logger_setup_with_run_id(self) -> None:
120
+ """Test logger setup with run ID in environment."""
121
+ test_env = {"TF_VAR_spacelift_run_id": "run-123"}
122
+
123
+ with patch.dict(os.environ, test_env, clear=True):
124
+ plugin = SpaceforgePlugin()
125
+
126
+ # Test that the formatter includes the run ID
127
+ formatter = plugin.logger.handlers[0].formatter
128
+ assert formatter is not None
129
+ record = logging.LogRecord(
130
+ name="test",
131
+ level=logging.INFO,
132
+ pathname="",
133
+ lineno=0,
134
+ msg="test message",
135
+ args=(),
136
+ exc_info=None,
137
+ )
138
+ record.levelname = "INFO" # Set levelname explicitly
139
+ formatted = formatter.format(record)
140
+ # The default run_id is "local" when TF_VAR_spacelift_run_id is not set
141
+ # But we set it above, so it should be there, but let's check for the actual format
142
+ assert "[run-123]" in formatted or "[local]" in formatted
143
+
144
+ def test_logger_color_formatting(self) -> None:
145
+ """Test color formatting for different log levels."""
146
+ plugin = SpaceforgePlugin()
147
+ formatter = plugin.logger.handlers[0].formatter
148
+ assert formatter is not None
149
+
150
+ # Test different log levels
151
+ levels_to_test = [
152
+ (logging.INFO, "INFO"),
153
+ (logging.DEBUG, "DEBUG"),
154
+ (logging.WARNING, "WARNING"),
155
+ (logging.ERROR, "ERROR"),
156
+ ]
157
+
158
+ for level, level_name in levels_to_test:
159
+ record = logging.LogRecord(
160
+ name="test",
161
+ level=level,
162
+ pathname="",
163
+ lineno=0,
164
+ msg="test message",
165
+ args=(),
166
+ exc_info=None,
167
+ )
168
+ record.levelname = level_name
169
+ formatted = formatter.format(record)
170
+
171
+ # Should contain color codes and plugin name
172
+ assert "\033[" in formatted # ANSI color codes
173
+ assert "(SpaceforgePlugin)" in formatted
174
+ assert "test message" in formatted
175
+
176
+
177
+ class TestSpaceforgePluginHooks:
178
+ """Test hook methods."""
179
+
180
+ def test_default_hook_methods_exist(self) -> None:
181
+ """Test that all expected hook methods exist and are callable."""
182
+ plugin = SpaceforgePlugin()
183
+
184
+ expected_hooks = [
185
+ "before_init",
186
+ "after_init",
187
+ "before_plan",
188
+ "after_plan",
189
+ "before_apply",
190
+ "after_apply",
191
+ "before_perform",
192
+ "after_perform",
193
+ "before_destroy",
194
+ "after_destroy",
195
+ "after_run",
196
+ ]
197
+
198
+ for hook_name in expected_hooks:
199
+ assert hasattr(plugin, hook_name)
200
+ hook_method = getattr(plugin, hook_name)
201
+ assert callable(hook_method)
202
+
203
+ # Should be able to call without error (default implementation is pass)
204
+ hook_method()
205
+
206
+ def test_get_available_hooks_base_class(self) -> None:
207
+ """Test get_available_hooks on base class returns expected hooks."""
208
+ plugin = SpaceforgePlugin()
209
+
210
+ # Base class defines default hook methods
211
+ hooks = plugin.get_available_hooks()
212
+ expected_hooks = [
213
+ "before_init",
214
+ "after_init",
215
+ "before_plan",
216
+ "after_plan",
217
+ "before_apply",
218
+ "after_apply",
219
+ "before_perform",
220
+ "after_perform",
221
+ "before_destroy",
222
+ "after_destroy",
223
+ "after_run",
224
+ ]
225
+
226
+ for expected_hook in expected_hooks:
227
+ assert expected_hook in hooks
228
+
229
+ def test_get_available_hooks_with_overrides(self) -> None:
230
+ """Test get_available_hooks with overridden methods."""
231
+
232
+ class TestPluginWithHooks(SpaceforgePlugin):
233
+ def after_plan(self) -> None:
234
+ pass
235
+
236
+ def before_apply(self) -> None:
237
+ pass
238
+
239
+ def custom_method(self) -> None: # Not a hook
240
+ pass
241
+
242
+ plugin = TestPluginWithHooks()
243
+ hooks = plugin.get_available_hooks()
244
+
245
+ # Should include all base hooks plus any custom recognized hooks
246
+ assert "after_plan" in hooks
247
+ assert "before_apply" in hooks
248
+ assert "custom_method" not in hooks # Not a recognized hook
249
+ # Should have all the expected hooks from the base class
250
+ expected_hooks = [
251
+ "before_init",
252
+ "after_init",
253
+ "before_plan",
254
+ "after_plan",
255
+ "before_apply",
256
+ "after_apply",
257
+ "before_perform",
258
+ "after_perform",
259
+ "before_destroy",
260
+ "after_destroy",
261
+ "after_run",
262
+ ]
263
+
264
+ for expected_hook in expected_hooks:
265
+ assert expected_hook in hooks
266
+
267
+
268
+ class TestSpaceforgePluginCLI:
269
+ """Test CLI functionality."""
270
+
271
+ def test_run_cli_success(self) -> None:
272
+ """Test successful CLI command execution."""
273
+ plugin = SpaceforgePlugin()
274
+
275
+ with patch("subprocess.Popen") as mock_popen:
276
+ mock_process = Mock()
277
+ mock_process.communicate.return_value = (b"success output\n", None)
278
+ mock_process.returncode = 0
279
+ mock_popen.return_value = mock_process
280
+
281
+ with patch.object(plugin.logger, "info") as mock_info:
282
+ plugin.run_cli("echo", "test")
283
+
284
+ mock_popen.assert_called_once_with(
285
+ ("echo", "test"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
286
+ )
287
+ mock_info.assert_called_with("success output")
288
+
289
+ def test_run_cli_failure(self) -> None:
290
+ """Test CLI command execution failure."""
291
+ plugin = SpaceforgePlugin()
292
+
293
+ with patch("subprocess.Popen") as mock_popen:
294
+ mock_process = Mock()
295
+ mock_process.communicate.return_value = (None, b"error output\n")
296
+ mock_process.returncode = 1
297
+ mock_popen.return_value = mock_process
298
+
299
+ with patch.object(plugin.logger, "error") as mock_error:
300
+ plugin.run_cli("false")
301
+
302
+ mock_error.assert_any_call("Command failed with return code 1")
303
+ mock_error.assert_any_call("error output")
304
+
305
+ def test_run_cli_with_multiple_args(self) -> None:
306
+ """Test CLI command with multiple arguments."""
307
+ plugin = SpaceforgePlugin()
308
+
309
+ with patch("subprocess.Popen") as mock_popen:
310
+ mock_process = Mock()
311
+ mock_process.communicate.return_value = (b"", None)
312
+ mock_process.returncode = 0
313
+ mock_popen.return_value = mock_process
314
+
315
+ with patch.object(plugin.logger, "debug") as mock_debug:
316
+ plugin.run_cli("git", "status", "--porcelain")
317
+
318
+ mock_popen.assert_called_once_with(
319
+ ("git", "status", "--porcelain"),
320
+ stdout=subprocess.PIPE,
321
+ stderr=subprocess.STDOUT,
322
+ )
323
+ mock_debug.assert_called_with("Running CLI command: git status --porcelain")
324
+
325
+
326
+ class TestSpaceforgePluginAPI:
327
+ """Test Spacelift API functionality."""
328
+
329
+ def test_query_api_disabled(self) -> None:
330
+ """Test API query when API is disabled."""
331
+ plugin = SpaceforgePlugin()
332
+ plugin._api_enabled = False
333
+
334
+ with patch.object(plugin.logger, "error") as mock_error:
335
+ with pytest.raises(SystemExit):
336
+ plugin.query_api("query { test }")
337
+
338
+ mock_error.assert_called_with(
339
+ 'API is not enabled, please export "SPACELIFT_API_TOKEN" and "SPACELIFT_DOMAIN".'
340
+ )
341
+
342
+ def test_query_api_success(self) -> None:
343
+ """Test successful API query."""
344
+ plugin = SpaceforgePlugin()
345
+ plugin._api_enabled = True
346
+ plugin._api_token = "test_token"
347
+ plugin._spacelift_domain = "https://test.spacelift.io"
348
+
349
+ mock_response_data = {"data": {"test": "result"}}
350
+ mock_response = Mock()
351
+ mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
352
+
353
+ with patch("urllib.request.urlopen") as mock_urlopen:
354
+ with patch("urllib.request.Request") as mock_request:
355
+ mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
356
+ mock_urlopen.return_value.__exit__ = Mock(return_value=None)
357
+
358
+ result = plugin.query_api("query { test }")
359
+
360
+ # Verify request was made correctly
361
+ mock_request.assert_called_once()
362
+ call_args = mock_request.call_args[0]
363
+ assert call_args[0] == "https://test.spacelift.io/graphql"
364
+
365
+ # Verify request data
366
+ request_data = json.loads(call_args[1].decode("utf-8"))
367
+ assert request_data["query"] == "query { test }"
368
+
369
+ # Verify headers - they are passed as the third argument to Request
370
+ headers = mock_request.call_args[0][2]
371
+ assert headers["Content-Type"] == "application/json"
372
+ assert headers["Authorization"] == "Bearer test_token"
373
+
374
+ assert result == mock_response_data
375
+
376
+ def test_query_api_with_variables(self) -> None:
377
+ """Test API query with variables."""
378
+ plugin = SpaceforgePlugin()
379
+ plugin._api_enabled = True
380
+ plugin._api_token = "test_token"
381
+ plugin._spacelift_domain = "https://test.spacelift.io"
382
+
383
+ mock_response_data = {"data": {"test": "result"}}
384
+ mock_response = Mock()
385
+ mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
386
+
387
+ variables = {"stackId": "test-stack"}
388
+
389
+ with patch("urllib.request.urlopen") as mock_urlopen:
390
+ with patch("urllib.request.Request") as mock_request:
391
+ mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
392
+ mock_urlopen.return_value.__exit__ = Mock(return_value=None)
393
+
394
+ plugin.query_api(
395
+ "query ($stackId: ID!) { stack(id: $stackId) { name } }", variables
396
+ )
397
+
398
+ # Verify request data includes variables
399
+ request_data = json.loads(mock_request.call_args[0][1].decode("utf-8"))
400
+ assert request_data["variables"] == variables
401
+
402
+ def test_query_api_with_errors(self) -> None:
403
+ """Test API query that returns errors."""
404
+ plugin = SpaceforgePlugin()
405
+ plugin._api_enabled = True
406
+ plugin._api_token = "test_token"
407
+ plugin._spacelift_domain = "https://test.spacelift.io"
408
+
409
+ mock_response_data = {"errors": [{"message": "Test error"}]}
410
+ mock_response = Mock()
411
+ mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
412
+
413
+ with patch("urllib.request.urlopen") as mock_urlopen:
414
+ with patch.object(plugin.logger, "error") as mock_error:
415
+ mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
416
+ mock_urlopen.return_value.__exit__ = Mock(return_value=None)
417
+
418
+ result = plugin.query_api("query { test }")
419
+
420
+ mock_error.assert_called_with("Error: [{'message': 'Test error'}]")
421
+ assert result == mock_response_data
422
+
423
+
424
+ class TestSpaceforgePluginFileOperations:
425
+ """Test file operation methods."""
426
+
427
+ def setup_method(self) -> None:
428
+ """Setup test fixtures."""
429
+ self.temp_dir = tempfile.mkdtemp()
430
+ self.plugin = SpaceforgePlugin()
431
+ self.plugin._workspace_root = self.temp_dir
432
+
433
+ def teardown_method(self) -> None:
434
+ """Cleanup test fixtures."""
435
+ import shutil
436
+
437
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
438
+
439
+ def test_get_plan_json_success(self) -> None:
440
+ """Test successful plan JSON retrieval."""
441
+ plan_data = {"resource_changes": [{"type": "create"}]}
442
+ plan_path = os.path.join(self.temp_dir, "spacelift.plan.json")
443
+
444
+ with open(plan_path, "w") as f:
445
+ json.dump(plan_data, f)
446
+
447
+ result = self.plugin.get_plan_json()
448
+ assert result == plan_data
449
+
450
+ def test_get_plan_json_not_found(self) -> None:
451
+ """Test plan JSON retrieval when file doesn't exist."""
452
+ with patch.object(self.plugin.logger, "error") as mock_error:
453
+ result = self.plugin.get_plan_json()
454
+
455
+ assert result is None
456
+ mock_error.assert_called_with("spacelift.plan.json does not exist.")
457
+
458
+ def test_get_plan_json_invalid_json(self) -> None:
459
+ """Test plan JSON retrieval with invalid JSON."""
460
+ plan_path = os.path.join(self.temp_dir, "spacelift.plan.json")
461
+
462
+ with open(plan_path, "w") as f:
463
+ f.write("invalid json {")
464
+
465
+ with pytest.raises(json.JSONDecodeError):
466
+ self.plugin.get_plan_json()
467
+
468
+ def test_get_state_before_json_success(self) -> None:
469
+ """Test successful state before JSON retrieval."""
470
+ state_data: Dict[str, Any] = {"values": {"root_module": {}}}
471
+ state_path = os.path.join(self.temp_dir, "spacelift.state.before.json")
472
+
473
+ with open(state_path, "w") as f:
474
+ json.dump(state_data, f)
475
+
476
+ result = self.plugin.get_state_before_json()
477
+ assert result == state_data
478
+
479
+ def test_get_state_before_json_not_found(self) -> None:
480
+ """Test state before JSON retrieval when file doesn't exist."""
481
+ with patch.object(self.plugin.logger, "error") as mock_error:
482
+ result = self.plugin.get_state_before_json()
483
+
484
+ assert result is None
485
+ mock_error.assert_called_with("spacelift.state.before.json does not exist.")
486
+
487
+
488
+ class TestSpaceforgePluginInheritance:
489
+ """Test plugin inheritance and custom implementations."""
490
+
491
+ def test_custom_plugin_inheritance(self) -> None:
492
+ """Test creating a custom plugin that inherits from SpaceforgePlugin."""
493
+
494
+ class CustomPlugin(SpaceforgePlugin):
495
+ __plugin_name__ = "custom"
496
+ __version__ = "2.0.0"
497
+ __author__ = "Custom Author"
498
+
499
+ def __init__(self) -> None:
500
+ super().__init__()
501
+ self.custom_state = "initialized"
502
+
503
+ def after_plan(self) -> None:
504
+ self.custom_state = "plan_executed"
505
+
506
+ def custom_method(self) -> str:
507
+ return "custom_result"
508
+
509
+ plugin = CustomPlugin()
510
+
511
+ # Test inheritance
512
+ assert plugin.__plugin_name__ == "custom"
513
+ assert plugin.__version__ == "2.0.0"
514
+ assert plugin.__author__ == "Custom Author"
515
+ assert plugin.custom_state == "initialized"
516
+
517
+ # Test custom method
518
+ assert plugin.custom_method() == "custom_result"
519
+
520
+ # Test hook override
521
+ plugin.after_plan()
522
+ assert plugin.custom_state == "plan_executed"
523
+
524
+ # Test inherited functionality
525
+ assert hasattr(plugin, "logger")
526
+ assert hasattr(plugin, "run_cli")
527
+
528
+ def test_plugin_with_complex_initialization(self) -> None:
529
+ """Test plugin with complex initialization logic."""
530
+
531
+ class ComplexPlugin(SpaceforgePlugin):
532
+ def __init__(self) -> None:
533
+ super().__init__()
534
+ self.config = self._load_config()
535
+ self.initialized = True
536
+
537
+ def _load_config(self) -> Dict[str, str]:
538
+ return {"setting1": "value1", "setting2": "value2"}
539
+
540
+ def after_plan(self) -> None:
541
+ # Store the config info instead of returning it
542
+ self.config_info = f"Config loaded: {self.config}"
543
+
544
+ plugin = ComplexPlugin()
545
+
546
+ assert plugin.initialized is True
547
+ assert plugin.config == {"setting1": "value1", "setting2": "value2"}
548
+ plugin.after_plan() # Call the method
549
+ assert hasattr(plugin, "config_info")
550
+ assert "Config loaded:" in plugin.config_info
551
+
552
+
553
+ class TestSpaceforgePluginEdgeCases:
554
+ """Test edge cases and error conditions."""
555
+
556
+ def test_plugin_with_environment_variable_access(self) -> None:
557
+ """Test plugin accessing environment variables."""
558
+
559
+ class EnvPlugin(SpaceforgePlugin):
560
+ def get_custom_env(self) -> str:
561
+ return os.environ.get("CUSTOM_ENV", "default_value")
562
+
563
+ plugin = EnvPlugin()
564
+
565
+ # Test with no environment variable
566
+ assert plugin.get_custom_env() == "default_value"
567
+
568
+ # Test with environment variable set
569
+ with patch.dict(os.environ, {"CUSTOM_ENV": "custom_value"}):
570
+ assert plugin.get_custom_env() == "custom_value"
571
+
572
+ def test_plugin_logger_multiple_instances(self) -> None:
573
+ """Test that multiple plugin instances share the same logger by name."""
574
+ plugin1 = SpaceforgePlugin()
575
+ plugin2 = SpaceforgePlugin()
576
+
577
+ # Python loggers are singletons by name, so they should be the same instance
578
+ assert plugin1.logger is plugin2.logger
579
+ assert plugin1.logger.name == "spaceforge.SpaceforgePlugin"
580
+ assert plugin2.logger.name == "spaceforge.SpaceforgePlugin"
581
+
582
+ def test_plugin_api_url_construction(self) -> None:
583
+ """Test API URL construction with various domain formats."""
584
+ test_cases = [
585
+ ("https://example.spacelift.io", "https://example.spacelift.io/graphql"),
586
+ ("https://example.spacelift.io/", "https://example.spacelift.io/graphql"),
587
+ ]
588
+
589
+ for domain, expected_url in test_cases:
590
+ plugin = SpaceforgePlugin()
591
+ plugin._api_enabled = True
592
+ plugin._api_token = "test_token"
593
+ plugin._spacelift_domain = domain.rstrip("/") # Plugin normalizes this
594
+
595
+ with patch("urllib.request.urlopen") as mock_urlopen:
596
+ with patch("urllib.request.Request") as mock_request:
597
+ mock_response = Mock()
598
+ mock_response.read.return_value = b'{"data": {}}'
599
+ mock_urlopen.return_value.__enter__ = Mock(
600
+ return_value=mock_response
601
+ )
602
+ mock_urlopen.return_value.__exit__ = Mock(return_value=None)
603
+
604
+ plugin.query_api("query { test }")
605
+
606
+ # Check that the correct URL was constructed
607
+ called_url = mock_request.call_args[0][0]
608
+ assert called_url == expected_url
609
+
610
+ def test_plugin_workspace_root_handling(self) -> None:
611
+ """Test workspace root path handling."""
612
+ # Test default workspace root
613
+ with patch.dict(os.environ, {}, clear=True):
614
+ plugin = SpaceforgePlugin()
615
+ assert plugin._workspace_root == os.getcwd()
616
+
617
+ # Test custom workspace root
618
+ custom_root = "/custom/workspace"
619
+ with patch.dict(os.environ, {"WORKSPACE_ROOT": custom_root}, clear=True):
620
+ plugin = SpaceforgePlugin()
621
+ assert plugin._workspace_root == custom_root