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,671 @@
1
+ import os
2
+ import tempfile
3
+ from typing import Optional
4
+ from unittest.mock import Mock, mock_open, patch
5
+
6
+ import pytest
7
+
8
+ from spaceforge.cls import (
9
+ Binary,
10
+ Context,
11
+ MountedFile,
12
+ Parameter,
13
+ PluginManifest,
14
+ Policy,
15
+ Variable,
16
+ Webhook,
17
+ )
18
+ from spaceforge.generator import PluginGenerator
19
+ from spaceforge.plugin import SpaceforgePlugin
20
+
21
+
22
+ class PluginExample(SpaceforgePlugin):
23
+ """Test plugin for generator testing."""
24
+
25
+ __plugin_name__ = "test_plugin"
26
+ __version__ = "2.0.0"
27
+ __author__ = "Test Author"
28
+
29
+ __parameters__ = [
30
+ Parameter(
31
+ name="api_key",
32
+ description="API key for authentication",
33
+ required=True,
34
+ sensitive=True,
35
+ ),
36
+ Parameter(
37
+ name="endpoint",
38
+ description="API endpoint URL",
39
+ required=False,
40
+ default="https://api.example.com",
41
+ ),
42
+ ]
43
+
44
+ __binaries__ = [
45
+ Binary(
46
+ name="test-cli",
47
+ download_urls={
48
+ "amd64": "https://example.com/test-cli-amd64",
49
+ "arm64": "https://example.com/test-cli-arm64",
50
+ },
51
+ )
52
+ ]
53
+
54
+ __contexts__ = [
55
+ Context(
56
+ name_prefix="test_context",
57
+ description="Test context",
58
+ labels={"env": "test"},
59
+ env=[Variable(key="TEST_VAR", value="test_value")],
60
+ )
61
+ ]
62
+
63
+ __webhooks__ = [
64
+ Webhook(
65
+ name_prefix="test_webhook",
66
+ endpoint="https://webhook.example.com",
67
+ secrets=[Variable(key="SECRET_KEY", value_from_parameter="api_key")],
68
+ )
69
+ ]
70
+
71
+ __policies__ = [
72
+ Policy(
73
+ name_prefix="test_policy",
74
+ type="notification",
75
+ body="package test",
76
+ labels={"type": "security"},
77
+ )
78
+ ]
79
+
80
+ def after_plan(self) -> None:
81
+ """Override hook method."""
82
+ pass
83
+
84
+ def before_apply(self) -> None:
85
+ """Override hook method."""
86
+ pass
87
+
88
+
89
+ class TestPluginGenerator:
90
+
91
+ def setup_method(self) -> None:
92
+ """Setup test fixtures."""
93
+ self.temp_dir = tempfile.mkdtemp()
94
+ self.test_plugin_path = os.path.join(self.temp_dir, "plugin.py")
95
+ self.test_output_path = os.path.join(self.temp_dir, "plugin.yaml")
96
+
97
+ # Create a test plugin file
98
+ with open(self.test_plugin_path, "w") as f:
99
+ f.write(
100
+ """
101
+ from spaceforge import SpaceforgePlugin, Parameter
102
+
103
+ class TestPlugin(SpaceforgePlugin):
104
+ __plugin_name__ = "test"
105
+ __version__ = "1.0.0"
106
+ __author__ = "Test"
107
+
108
+ __parameters__ = [
109
+ Parameter(name="test_param", description="Test parameter", required=False, default="default_value")
110
+ ]
111
+
112
+ def after_plan(self) -> None:
113
+ pass
114
+ """
115
+ )
116
+
117
+ def teardown_method(self) -> None:
118
+ """Cleanup test fixtures."""
119
+ import shutil
120
+
121
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
122
+
123
+ def test_plugin_generator_init(self) -> None:
124
+ """Test PluginGenerator initialization."""
125
+ generator = PluginGenerator("plugin_test.py", "output_test.yaml")
126
+
127
+ assert generator.plugin_path == "plugin_test.py"
128
+ assert generator.output_path == "output_test.yaml"
129
+ assert generator.plugin_class is None
130
+ assert generator.plugin_instance is None
131
+ assert generator.plugin_working_directory is None
132
+
133
+ def test_plugin_generator_init_defaults(self) -> None:
134
+ """Test PluginGenerator initialization with default values."""
135
+ generator = PluginGenerator()
136
+
137
+ assert generator.plugin_path == "plugin.py"
138
+ assert generator.output_path == "plugin.yaml"
139
+
140
+ def test_load_plugin_file_not_found(self) -> None:
141
+ """Test loading plugin when file doesn't exist."""
142
+ generator = PluginGenerator("nonexistent.py")
143
+
144
+ with pytest.raises(FileNotFoundError, match="Plugin file not found"):
145
+ generator.load_plugin()
146
+
147
+ def test_load_plugin_invalid_module(self) -> None:
148
+ """Test loading invalid Python module."""
149
+ invalid_path = os.path.join(self.temp_dir, "invalid.py")
150
+ with open(invalid_path, "w") as f:
151
+ f.write("invalid python syntax }")
152
+
153
+ generator = PluginGenerator(invalid_path)
154
+
155
+ with pytest.raises(Exception): # Could be syntax error or import error
156
+ generator.load_plugin()
157
+
158
+ def test_load_plugin_no_spacepy_subclass(self) -> None:
159
+ """Test loading plugin with no SpaceforgePlugin subclass."""
160
+ no_plugin_path = os.path.join(self.temp_dir, "no_plugin.py")
161
+ with open(no_plugin_path, "w") as f:
162
+ f.write(
163
+ """
164
+ class NotAPlugin:
165
+ pass
166
+ """
167
+ )
168
+
169
+ generator = PluginGenerator(no_plugin_path)
170
+
171
+ with pytest.raises(ValueError, match="No SpaceforgePlugin subclass found"):
172
+ generator.load_plugin()
173
+
174
+ def test_load_plugin_success(self) -> None:
175
+ """Test successful plugin loading."""
176
+ generator = PluginGenerator(self.test_plugin_path)
177
+ generator.load_plugin()
178
+
179
+ assert generator.plugin_class is not None
180
+ assert generator.plugin_instance is not None
181
+ assert generator.plugin_class.__name__ == "TestPlugin"
182
+ assert generator.plugin_working_directory == "/mnt/workspace/plugins/test"
183
+
184
+ @patch("spaceforge.generator.importlib.util.spec_from_file_location")
185
+ def test_load_plugin_spec_none(self, mock_spec: Mock) -> None:
186
+ """Test plugin loading when spec is None."""
187
+ mock_spec.return_value = None
188
+
189
+ generator = PluginGenerator(self.test_plugin_path)
190
+
191
+ with pytest.raises(ImportError, match="Could not load plugin"):
192
+ generator.load_plugin()
193
+
194
+ def test_get_plugin_metadata_with_all_attributes(self) -> None:
195
+ """Test metadata extraction with all attributes present."""
196
+ generator = PluginGenerator()
197
+ generator.plugin_class = PluginExample
198
+
199
+ metadata = generator.get_plugin_metadata()
200
+
201
+ assert metadata["name_prefix"] == "test_plugin"
202
+ assert metadata["version"] == "2.0.0"
203
+ assert metadata["author"] == "Test Author"
204
+ assert metadata["description"] == "Test plugin for generator testing."
205
+
206
+ def test_get_plugin_metadata_with_defaults(self) -> None:
207
+ """Test metadata extraction with missing attributes."""
208
+
209
+ class MinimalPlugin(SpaceforgePlugin):
210
+ pass
211
+
212
+ generator = PluginGenerator()
213
+ generator.plugin_class = MinimalPlugin
214
+
215
+ metadata = generator.get_plugin_metadata()
216
+
217
+ # MinimalPlugin inherits __plugin_name__ from SpaceforgePlugin
218
+ assert metadata["name_prefix"] == "SpaceforgePlugin"
219
+ assert metadata["version"] == "1.0.0" # inherited from base
220
+ assert metadata["author"] == "Spacelift Team" # inherited from base
221
+ assert "MinimalPlugin" in metadata["description"]
222
+
223
+ def test_get_plugin_metadata_class_name_fallback(self) -> None:
224
+ """Test metadata extraction using class name when __plugin_name__ not set."""
225
+
226
+ # Test the fallback behavior by mocking the plugin class itself
227
+ class MinimalPlugin: # Don't inherit from SpaceforgePlugin
228
+ __name__ = "MinimalPlugin"
229
+
230
+ generator = PluginGenerator()
231
+ generator.plugin_class = MinimalPlugin # type: ignore[assignment]
232
+
233
+ metadata = generator.get_plugin_metadata()
234
+
235
+ # Should use class name fallback logic
236
+ assert (
237
+ metadata["name_prefix"] == "minimal"
238
+ ) # class name lowercased with 'plugin' removed
239
+ assert metadata["version"] == "1.0.0" # default
240
+ assert metadata["author"] == "Unknown" # default
241
+ assert "MinimalPlugin" in metadata["description"]
242
+
243
+ def test_get_plugin_parameters(self) -> None:
244
+ """Test parameter extraction."""
245
+ generator = PluginGenerator()
246
+ generator.plugin_class = PluginExample
247
+
248
+ parameters = generator.get_plugin_parameters()
249
+
250
+ assert parameters is not None
251
+ assert len(parameters) == 2
252
+ assert parameters[0].name == "api_key"
253
+ assert parameters[0].sensitive is True
254
+ assert parameters[1].name == "endpoint"
255
+ assert parameters[1].default == "https://api.example.com"
256
+
257
+ def test_get_plugin_parameters_none(self) -> None:
258
+ """Test parameter extraction when no parameters defined."""
259
+
260
+ class NoParamsPlugin(SpaceforgePlugin):
261
+ pass
262
+
263
+ generator = PluginGenerator()
264
+ generator.plugin_class = NoParamsPlugin
265
+
266
+ parameters = generator.get_plugin_parameters()
267
+ assert parameters is None
268
+
269
+ def test_get_available_hooks(self) -> None:
270
+ """Test hook method detection."""
271
+ generator = PluginGenerator()
272
+ generator.plugin_class = PluginExample
273
+
274
+ hooks = generator.get_available_hooks()
275
+
276
+ assert "after_plan" in hooks
277
+ assert "before_apply" in hooks
278
+ assert len(hooks) == 2
279
+
280
+ def test_get_available_hooks_no_overrides(self) -> None:
281
+ """Test hook detection with no overridden methods."""
282
+
283
+ class NoHooksPlugin(SpaceforgePlugin):
284
+ pass
285
+
286
+ generator = PluginGenerator()
287
+ generator.plugin_class = NoHooksPlugin
288
+
289
+ hooks = generator.get_available_hooks()
290
+ assert hooks == []
291
+
292
+ def test_get_plugin_binaries(self) -> None:
293
+ """Test binary extraction."""
294
+ generator = PluginGenerator()
295
+ generator.plugin_class = PluginExample
296
+
297
+ binaries = generator.get_plugin_binaries()
298
+
299
+ assert binaries is not None
300
+ assert len(binaries) == 1
301
+ assert binaries[0].name == "test-cli"
302
+ assert "amd64" in binaries[0].download_urls
303
+ assert "arm64" in binaries[0].download_urls
304
+
305
+ def test_get_plugin_binaries_none(self) -> None:
306
+ """Test binary extraction when no binaries defined."""
307
+
308
+ class NoBinariesPlugin(SpaceforgePlugin):
309
+ pass
310
+
311
+ generator = PluginGenerator()
312
+ generator.plugin_class = NoBinariesPlugin
313
+
314
+ binaries = generator.get_plugin_binaries()
315
+ assert binaries is None
316
+
317
+ def test_generate_binary_install_command(self) -> None:
318
+ """Test binary installation command generation."""
319
+ generator = PluginGenerator()
320
+ generator.plugin_class = PluginExample
321
+
322
+ command = generator.generate_binary_install_command()
323
+
324
+ assert "mkdir -p /mnt/workspace/plugins/plugin_binaries" in command
325
+ assert "curl https://example.com/test-cli-amd64" in command
326
+ assert "curl https://example.com/test-cli-arm64" in command
327
+ assert "arch" in command
328
+ assert "x86_64" in command
329
+
330
+ def test_generate_binary_install_command_no_binaries(self) -> None:
331
+ """Test binary command generation when no binaries."""
332
+
333
+ class NoBinariesPlugin(SpaceforgePlugin):
334
+ pass
335
+
336
+ generator = PluginGenerator()
337
+ generator.plugin_class = NoBinariesPlugin
338
+
339
+ command = generator.generate_binary_install_command()
340
+ assert command == ""
341
+
342
+ def test_generate_binary_install_command_missing_urls(self) -> None:
343
+ """Test binary command generation with missing URLs."""
344
+
345
+ class InvalidBinaryPlugin(SpaceforgePlugin):
346
+ __binaries__ = [Binary(name="invalid", download_urls={})]
347
+
348
+ generator = PluginGenerator()
349
+ generator.plugin_class = InvalidBinaryPlugin
350
+
351
+ with pytest.raises(ValueError, match="must have at least one download URL"):
352
+ generator.generate_binary_install_command()
353
+
354
+ def test_get_plugin_policies(self) -> None:
355
+ """Test policy extraction."""
356
+ generator = PluginGenerator()
357
+ generator.plugin_class = PluginExample
358
+
359
+ policies = generator.get_plugin_policies()
360
+
361
+ assert policies is not None
362
+ assert len(policies) == 1
363
+ assert policies[0].name_prefix == "test_policy"
364
+ assert policies[0].type == "notification"
365
+ assert policies[0].body == "package test"
366
+
367
+ def test_get_plugin_webhooks(self) -> None:
368
+ """Test webhook extraction."""
369
+ generator = PluginGenerator()
370
+ generator.plugin_class = PluginExample
371
+
372
+ webhooks = generator.get_plugin_webhooks()
373
+
374
+ assert webhooks is not None
375
+ assert len(webhooks) == 1
376
+ assert webhooks[0].name_prefix == "test_webhook"
377
+ assert webhooks[0].endpoint == "https://webhook.example.com"
378
+
379
+ @patch("os.path.exists")
380
+ @patch("builtins.open", new_callable=mock_open, read_data="requirements content")
381
+ def test_get_plugin_contexts_with_requirements(
382
+ self, mock_file: Mock, mock_exists: Mock
383
+ ) -> None:
384
+ """Test context generation with requirements.txt."""
385
+ mock_exists.side_effect = (
386
+ lambda path: path == "requirements.txt" or "plugin.py" in path
387
+ )
388
+
389
+ generator = PluginGenerator(self.test_plugin_path)
390
+ generator.plugin_class = PluginExample
391
+ generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
392
+
393
+ contexts = generator.get_plugin_contexts()
394
+
395
+ assert len(contexts) == 1
396
+ context = contexts[0]
397
+
398
+ # Should have before_init hooks for venv setup
399
+ assert context.hooks is not None
400
+ assert "before_init" in context.hooks
401
+ venv_command = None
402
+ for cmd in context.hooks["before_init"]:
403
+ if "python -m venv" in cmd:
404
+ venv_command = cmd
405
+ break
406
+ assert venv_command is not None
407
+
408
+ # Should have requirements.txt as mounted file
409
+ assert context.mounted_files is not None
410
+ req_file = None
411
+ for mf in context.mounted_files:
412
+ if "requirements.txt" in mf.path:
413
+ req_file = mf
414
+ break
415
+ assert req_file is not None
416
+ assert req_file.content == "requirements content"
417
+
418
+ @patch("os.path.exists")
419
+ @patch("builtins.open", new_callable=mock_open, read_data="plugin content")
420
+ def test_get_plugin_contexts_basic(
421
+ self, mock_file: Mock, mock_exists: Mock
422
+ ) -> None:
423
+ """Test basic context generation."""
424
+ mock_exists.side_effect = lambda path: "plugin.py" in path
425
+
426
+ generator = PluginGenerator(self.test_plugin_path)
427
+ generator.plugin_class = PluginExample
428
+ generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
429
+
430
+ with patch.object(
431
+ generator, "get_available_hooks", return_value=["after_plan"]
432
+ ):
433
+ contexts = generator.get_plugin_contexts()
434
+
435
+ assert len(contexts) == 1
436
+ context = contexts[0]
437
+
438
+ # Should have plugin file mounted
439
+ assert context.mounted_files is not None
440
+ plugin_file = None
441
+ for mf in context.mounted_files:
442
+ if "plugin.py" in mf.path:
443
+ plugin_file = mf
444
+ break
445
+ assert plugin_file is not None
446
+
447
+ # Should have spacepy runner hooks
448
+ assert context.hooks is not None
449
+ assert "after_plan" in context.hooks
450
+ runner_command = context.hooks["after_plan"][0]
451
+ assert "python -m spaceforge runner" in runner_command
452
+ assert "after_plan" in runner_command
453
+
454
+ def test_generate_manifest(self) -> None:
455
+ """Test complete manifest generation."""
456
+ generator = PluginGenerator(self.test_plugin_path)
457
+
458
+ with patch.object(generator, "load_plugin"):
459
+ generator.plugin_class = PluginExample
460
+ manifest = generator.generate_manifest()
461
+
462
+ assert isinstance(manifest, PluginManifest)
463
+ assert manifest.version == "2.0.0"
464
+ assert manifest.author == "Test Author"
465
+ assert manifest.description == "Test plugin for generator testing."
466
+ assert manifest.parameters is not None
467
+ assert manifest.contexts is not None
468
+ assert manifest.webhooks is not None
469
+ assert manifest.policies is not None
470
+
471
+ @patch("builtins.open", new_callable=mock_open)
472
+ @patch("spaceforge.generator.yaml.dump")
473
+ def test_write_yaml(self, mock_yaml_dump: Mock, mock_file: Mock) -> None:
474
+ """Test YAML writing functionality."""
475
+ generator = PluginGenerator(output_path=self.test_output_path)
476
+ manifest = PluginManifest(
477
+ name_prefix="test",
478
+ version="1.0.0",
479
+ description="Test",
480
+ author="Test Author",
481
+ )
482
+
483
+ generator.write_yaml(manifest)
484
+
485
+ mock_file.assert_called_once_with(self.test_output_path, "w")
486
+ mock_yaml_dump.assert_called_once()
487
+
488
+ # Verify yaml.dump was called with correct parameters
489
+ args, kwargs = mock_yaml_dump.call_args
490
+ assert args[0] == manifest
491
+ assert kwargs["default_flow_style"] is False
492
+ assert kwargs["sort_keys"] is False
493
+ assert kwargs["indent"] == 2
494
+
495
+ @patch.object(PluginGenerator, "write_yaml")
496
+ @patch.object(PluginGenerator, "generate_manifest")
497
+ def test_generate(
498
+ self, mock_generate_manifest: Mock, mock_write_yaml: Mock
499
+ ) -> None:
500
+ """Test complete generate method."""
501
+ generator = PluginGenerator()
502
+ mock_manifest = PluginManifest(
503
+ name_prefix="test", version="1.0.0", description="Test", author="Test"
504
+ )
505
+ mock_generate_manifest.return_value = mock_manifest
506
+
507
+ generator.generate()
508
+
509
+ mock_generate_manifest.assert_called_once()
510
+ mock_write_yaml.assert_called_once_with(mock_manifest)
511
+
512
+ def test_integration_full_workflow(self) -> None:
513
+ """Integration test for complete workflow."""
514
+ # Create a complete test plugin file
515
+ full_plugin_path = os.path.join(self.temp_dir, "full_plugin.py")
516
+ with open(full_plugin_path, "w") as f:
517
+ f.write(
518
+ '''
519
+ from spaceforge import SpaceforgePlugin, Parameter
520
+
521
+ class FullTestPlugin(SpaceforgePlugin):
522
+ """A full test plugin."""
523
+
524
+ __plugin_name__ = "full_test"
525
+ __version__ = "1.5.0"
526
+ __author__ = "Integration Test"
527
+
528
+ __parameters__ = [
529
+ Parameter(
530
+ name="test_param",
531
+ description="Test parameter",
532
+ required=False,
533
+ default="test_value"
534
+ )
535
+ ]
536
+
537
+ def after_plan(self):
538
+ """Override after_plan hook."""
539
+ pass
540
+ '''
541
+ )
542
+
543
+ generator = PluginGenerator(full_plugin_path, self.test_output_path)
544
+
545
+ # This should work end-to-end
546
+ generator.load_plugin()
547
+ assert generator.plugin_class is not None
548
+ assert generator.plugin_class.__name__ == "FullTestPlugin"
549
+
550
+ metadata = generator.get_plugin_metadata()
551
+ assert metadata["name_prefix"] == "full_test"
552
+ assert metadata["version"] == "1.5.0"
553
+
554
+ hooks = generator.get_available_hooks()
555
+ assert "after_plan" in hooks
556
+
557
+ manifest = generator.generate_manifest()
558
+ assert manifest.version == "1.5.0"
559
+ assert manifest.description == "A full test plugin."
560
+
561
+
562
+ class TestPluginGeneratorEdgeCases:
563
+ """Test edge cases and error conditions."""
564
+
565
+ def test_plugin_with_docstring_multiline(self) -> None:
566
+ """Test plugin with multiline docstring."""
567
+
568
+ class MultilineDocPlugin(SpaceforgePlugin):
569
+ """
570
+ This is a multiline
571
+ docstring with multiple
572
+ lines of description.
573
+ """
574
+
575
+ pass
576
+
577
+ generator = PluginGenerator()
578
+ generator.plugin_class = MultilineDocPlugin
579
+
580
+ metadata = generator.get_plugin_metadata()
581
+ assert "multiline" in metadata["description"]
582
+ assert "multiple" in metadata["description"]
583
+
584
+ def test_plugin_class_name_with_plugin_suffix(self) -> None:
585
+ """Test plugin class name ending with 'Plugin'."""
586
+
587
+ class MyAwesomePlugin: # Don't inherit from SpaceforgePlugin
588
+ __name__ = "MyAwesomePlugin"
589
+
590
+ generator = PluginGenerator()
591
+ generator.plugin_class = MyAwesomePlugin # type: ignore[assignment]
592
+
593
+ metadata = generator.get_plugin_metadata()
594
+ assert metadata["name_prefix"] == "myawesome" # 'plugin' removed and lowercased
595
+
596
+ def test_binary_install_single_arch(self) -> None:
597
+ """Test binary installation with only one architecture."""
598
+
599
+ class SingleArchPlugin(SpaceforgePlugin):
600
+ __binaries__ = [
601
+ Binary(
602
+ name="single-arch",
603
+ download_urls={"amd64": "https://example.com/binary-amd64"},
604
+ )
605
+ ]
606
+
607
+ generator = PluginGenerator()
608
+ generator.plugin_class = SingleArchPlugin
609
+
610
+ command = generator.generate_binary_install_command()
611
+
612
+ assert "https://example.com/binary-amd64" in command
613
+ assert "arm64 binary not available" in command
614
+
615
+ def test_context_merging_with_existing(self) -> None:
616
+ """Test that generated hooks are merged with existing context hooks."""
617
+
618
+ class ExistingHooksPlugin(SpaceforgePlugin):
619
+ __plugin_name__ = "existing_hooks"
620
+
621
+ __contexts__ = [
622
+ Context(
623
+ name_prefix="existing",
624
+ description="Existing context",
625
+ hooks={"before_init": ["echo 'existing hook'"]},
626
+ mounted_files=[
627
+ MountedFile(
628
+ path="/existing", content="existing", sensitive=False
629
+ )
630
+ ],
631
+ env=[Variable(key="EXISTING", value="existing")],
632
+ )
633
+ ]
634
+
635
+ def after_plan(self) -> None:
636
+ pass
637
+
638
+ generator = PluginGenerator("/fake/path")
639
+ generator.plugin_class = ExistingHooksPlugin
640
+ generator.plugin_working_directory = "/mnt/workspace/plugins/existing_hooks"
641
+
642
+ with patch("os.path.exists") as mock_exists:
643
+ # Only plugin file exists, not requirements.txt
644
+ mock_exists.side_effect = lambda path: path == "/fake/path"
645
+ with patch("builtins.open", mock_open(read_data="fake content")):
646
+ contexts = generator.get_plugin_contexts()
647
+
648
+ context = contexts[0]
649
+
650
+ # The update() method replaces the existing hooks completely
651
+ assert context.hooks is not None
652
+ assert "after_plan" in context.hooks
653
+ assert any("mkdir -p" in cmd for cmd in context.hooks["before_init"])
654
+
655
+ # Should have both existing and generated mounted files
656
+ assert context.mounted_files is not None
657
+ existing_files = [mf for mf in context.mounted_files if mf.path == "/existing"]
658
+ assert len(existing_files) == 1
659
+
660
+ # Plugin file should be mounted (path contains the working directory)
661
+ plugin_files = [
662
+ mf
663
+ for mf in context.mounted_files
664
+ if "/mnt/workspace/plugins/existing_hooks" in mf.path
665
+ ]
666
+ assert len(plugin_files) == 1
667
+
668
+ # Should have existing env vars
669
+ assert context.env is not None
670
+ existing_vars = [var for var in context.env if var.key == "EXISTING"]
671
+ assert len(existing_vars) == 1