spaceforge 0.0.2__py3-none-any.whl → 0.0.3__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.
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import tempfile
3
- from typing import Optional
3
+ from typing import Dict, List
4
4
  from unittest.mock import Mock, mock_open, patch
5
5
 
6
6
  import pytest
@@ -318,8 +318,15 @@ class NotAPlugin:
318
318
  """Test binary installation command generation."""
319
319
  generator = PluginGenerator()
320
320
  generator.plugin_class = PluginExample
321
+ generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
322
+ generator.config = {
323
+ "setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
324
+ "plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
325
+ }
321
326
 
322
- command = generator.generate_binary_install_command()
327
+ hooks: Dict[str, List[str]] = {"before_init": []}
328
+ generator._generate_binary_install_command(hooks)
329
+ command = hooks["before_init"][-1]
323
330
 
324
331
  assert "mkdir -p /mnt/workspace/plugins/plugin_binaries" in command
325
332
  assert "curl https://example.com/test-cli-amd64" in command
@@ -335,9 +342,16 @@ class NotAPlugin:
335
342
 
336
343
  generator = PluginGenerator()
337
344
  generator.plugin_class = NoBinariesPlugin
345
+ generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
346
+ generator.config = {
347
+ "setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
348
+ "plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
349
+ }
338
350
 
339
- command = generator.generate_binary_install_command()
340
- assert command == ""
351
+ hooks: Dict[str, List[str]] = {"before_init": []}
352
+ generator._generate_binary_install_command(hooks)
353
+ # No binaries should mean no new commands added
354
+ assert len(hooks["before_init"]) == 0
341
355
 
342
356
  def test_generate_binary_install_command_missing_urls(self) -> None:
343
357
  """Test binary command generation with missing URLs."""
@@ -347,9 +361,15 @@ class NotAPlugin:
347
361
 
348
362
  generator = PluginGenerator()
349
363
  generator.plugin_class = InvalidBinaryPlugin
364
+ generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
365
+ generator.config = {
366
+ "setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
367
+ "plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
368
+ }
350
369
 
370
+ hooks: Dict[str, List[str]] = {"before_init": []}
351
371
  with pytest.raises(ValueError, match="must have at least one download URL"):
352
- generator.generate_binary_install_command()
372
+ generator._generate_binary_install_command(hooks)
353
373
 
354
374
  def test_get_plugin_policies(self) -> None:
355
375
  """Test policy extraction."""
@@ -389,6 +409,10 @@ class NotAPlugin:
389
409
  generator = PluginGenerator(self.test_plugin_path)
390
410
  generator.plugin_class = PluginExample
391
411
  generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
412
+ generator.config = {
413
+ "setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
414
+ "plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
415
+ }
392
416
 
393
417
  contexts = generator.get_plugin_contexts()
394
418
 
@@ -426,6 +450,10 @@ class NotAPlugin:
426
450
  generator = PluginGenerator(self.test_plugin_path)
427
451
  generator.plugin_class = PluginExample
428
452
  generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
453
+ generator.config = {
454
+ "setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
455
+ "plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
456
+ }
429
457
 
430
458
  with patch.object(
431
459
  generator, "get_available_hooks", return_value=["after_plan"]
@@ -447,16 +475,21 @@ class NotAPlugin:
447
475
  # Should have spacepy runner hooks
448
476
  assert context.hooks is not None
449
477
  assert "after_plan" in context.hooks
450
- runner_command = context.hooks["after_plan"][0]
478
+ runner_command = context.hooks["after_plan"][1]
451
479
  assert "python -m spaceforge runner" in runner_command
452
480
  assert "after_plan" in runner_command
453
481
 
454
482
  def test_generate_manifest(self) -> None:
455
483
  """Test complete manifest generation."""
456
484
  generator = PluginGenerator(self.test_plugin_path)
485
+ generator.plugin_class = PluginExample
486
+ generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
487
+ generator.config = {
488
+ "setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
489
+ "plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
490
+ }
457
491
 
458
492
  with patch.object(generator, "load_plugin"):
459
- generator.plugin_class = PluginExample
460
493
  manifest = generator.generate_manifest()
461
494
 
462
495
  assert isinstance(manifest, PluginManifest)
@@ -474,7 +507,7 @@ class NotAPlugin:
474
507
  """Test YAML writing functionality."""
475
508
  generator = PluginGenerator(output_path=self.test_output_path)
476
509
  manifest = PluginManifest(
477
- name_prefix="test",
510
+ name="test",
478
511
  version="1.0.0",
479
512
  description="Test",
480
513
  author="Test Author",
@@ -500,7 +533,7 @@ class NotAPlugin:
500
533
  """Test complete generate method."""
501
534
  generator = PluginGenerator()
502
535
  mock_manifest = PluginManifest(
503
- name_prefix="test", version="1.0.0", description="Test", author="Test"
536
+ name="test", version="1.0.0", description="Test", author="Test"
504
537
  )
505
538
  mock_generate_manifest.return_value = mock_manifest
506
539
 
@@ -606,8 +639,15 @@ class TestPluginGeneratorEdgeCases:
606
639
 
607
640
  generator = PluginGenerator()
608
641
  generator.plugin_class = SingleArchPlugin
642
+ generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
643
+ generator.config = {
644
+ "setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
645
+ "plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
646
+ }
609
647
 
610
- command = generator.generate_binary_install_command()
648
+ hooks: Dict[str, List[str]] = {"before_init": []}
649
+ generator._generate_binary_install_command(hooks)
650
+ command = hooks["before_init"][-1]
611
651
 
612
652
  assert "https://example.com/binary-amd64" in command
613
653
  assert "arm64 binary not available" in command
@@ -638,6 +678,10 @@ class TestPluginGeneratorEdgeCases:
638
678
  generator = PluginGenerator("/fake/path")
639
679
  generator.plugin_class = ExistingHooksPlugin
640
680
  generator.plugin_working_directory = "/mnt/workspace/plugins/existing_hooks"
681
+ generator.config = {
682
+ "setup_virtual_env": "cd /mnt/workspace/plugins/existing_hooks && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
683
+ "plugin_mounted_path": "/mnt/workspace/plugins/existing_hooks/plugin.py",
684
+ }
641
685
 
642
686
  with patch("os.path.exists") as mock_exists:
643
687
  # Only plugin file exists, not requirements.txt
@@ -669,3 +713,70 @@ class TestPluginGeneratorEdgeCases:
669
713
  assert context.env is not None
670
714
  existing_vars = [var for var in context.env if var.key == "EXISTING"]
671
715
  assert len(existing_vars) == 1
716
+
717
+ def test_parameters_and_variables_id_generation(self) -> None:
718
+ class ParametersAndVariablesPlugin(SpaceforgePlugin):
719
+ __plugin_name__ = "parameters_and_variables"
720
+
721
+ __parameters__ = [
722
+ Parameter(
723
+ name="api key",
724
+ description="API key for authentication",
725
+ required=True,
726
+ sensitive=True,
727
+ ),
728
+ Parameter(
729
+ name="endpoint",
730
+ description="API endpoint URL",
731
+ required=False,
732
+ default="https://api.example.com",
733
+ ),
734
+ ]
735
+
736
+ __contexts__ = [
737
+ Context(
738
+ name_prefix="existing",
739
+ description="Existing context",
740
+ hooks={"before_init": ["echo 'existing hook'"]},
741
+ mounted_files=[
742
+ MountedFile(
743
+ path="/existing", content="existing", sensitive=False
744
+ )
745
+ ],
746
+ env=[
747
+ Variable(
748
+ key="API_KEY",
749
+ value_from_parameter="api key",
750
+ sensitive=True,
751
+ ),
752
+ Variable(key="ENDPOINT", value_from_parameter="endpoint"),
753
+ ],
754
+ )
755
+ ]
756
+
757
+ def after_plan(self) -> None:
758
+ pass
759
+
760
+ generator = PluginGenerator("/fake/path")
761
+ generator.plugin_class = ParametersAndVariablesPlugin
762
+ generator.plugin_working_directory = (
763
+ "/mnt/workspace/plugins/parametersandvariables"
764
+ )
765
+ generator.config = {
766
+ "setup_virtual_env": "cd /mnt/workspace/plugins/parametersandvariables && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
767
+ "plugin_mounted_path": "/mnt/workspace/plugins/parametersandvariables/plugin.py",
768
+ }
769
+
770
+ with patch("os.path.exists") as mock_exists:
771
+ # Only plugin file exists, not requirements.txt
772
+ mock_exists.side_effect = lambda path: path == "/fake/path"
773
+ with patch("builtins.open", mock_open(read_data="fake content")):
774
+ contexts = generator.get_plugin_contexts()
775
+ parameters = generator.get_plugin_parameters()
776
+
777
+ assert contexts[0].env is not None
778
+ assert parameters is not None
779
+ assert parameters[0].id is not None
780
+ assert parameters[1].id is not None
781
+ assert parameters[0].id == contexts[0].env[0].value_from_parameter
782
+ assert parameters[1].id == contexts[0].env[1].value_from_parameter
@@ -0,0 +1,167 @@
1
+ """Tests for PluginGenerator binary handling."""
2
+
3
+ from typing import Dict, List
4
+
5
+ import pytest
6
+
7
+ from spaceforge.cls import Binary
8
+ from spaceforge.generator import PluginGenerator
9
+ from spaceforge.plugin import SpaceforgePlugin
10
+
11
+
12
+ class TestPluginGeneratorBinaries:
13
+ """Test binary extraction and installation command generation."""
14
+
15
+ def test_should_extract_binaries_when_defined(self) -> None:
16
+ """Should extract and return binary list when plugin defines them."""
17
+
18
+ # Arrange
19
+ class BinaryPlugin(SpaceforgePlugin):
20
+ __binaries__ = [
21
+ Binary(
22
+ name="test-cli",
23
+ download_urls={
24
+ "amd64": "https://example.com/test-cli-amd64",
25
+ "arm64": "https://example.com/test-cli-arm64",
26
+ },
27
+ )
28
+ ]
29
+
30
+ generator = PluginGenerator()
31
+ generator.plugin_class = BinaryPlugin
32
+
33
+ # Act
34
+ binaries = generator.get_plugin_binaries()
35
+
36
+ # Assert
37
+ assert binaries is not None
38
+ assert len(binaries) == 1
39
+ assert binaries[0].name == "test-cli"
40
+ assert "amd64" in binaries[0].download_urls
41
+ assert "arm64" in binaries[0].download_urls
42
+
43
+ def test_should_return_none_when_no_binaries_defined(self) -> None:
44
+ """Should return None when plugin has no binaries."""
45
+
46
+ # Arrange
47
+ class NoBinariesPlugin(SpaceforgePlugin):
48
+ pass
49
+
50
+ generator = PluginGenerator()
51
+ generator.plugin_class = NoBinariesPlugin
52
+
53
+ # Act
54
+ binaries = generator.get_plugin_binaries()
55
+
56
+ # Assert
57
+ assert binaries is None
58
+
59
+ def test_should_generate_binary_install_command_for_multi_arch(self) -> None:
60
+ """Should generate installation commands for multiple architectures."""
61
+
62
+ # Arrange
63
+ class MultiArchPlugin(SpaceforgePlugin):
64
+ __binaries__ = [
65
+ Binary(
66
+ name="multi-cli",
67
+ download_urls={
68
+ "amd64": "https://example.com/multi-cli-amd64",
69
+ "arm64": "https://example.com/multi-cli-arm64",
70
+ },
71
+ )
72
+ ]
73
+
74
+ generator = PluginGenerator()
75
+ generator.plugin_class = MultiArchPlugin
76
+ generator.plugin_working_directory = "/mnt/workspace/plugins/multiarch"
77
+ generator.config = {
78
+ "setup_virtual_env": "cd /mnt/workspace/plugins/multiarch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
79
+ "plugin_mounted_path": "/mnt/workspace/plugins/multiarch/plugin.py",
80
+ }
81
+
82
+ hooks: Dict[str, List[str]] = {"before_init": []}
83
+
84
+ # Act
85
+ generator._generate_binary_install_command(hooks)
86
+ command = hooks["before_init"][-1]
87
+
88
+ # Assert
89
+ assert "mkdir -p /mnt/workspace/plugins/plugin_binaries" in command
90
+ assert "curl https://example.com/multi-cli-amd64" in command
91
+ assert "curl https://example.com/multi-cli-arm64" in command
92
+ assert "arch" in command
93
+ assert "x86_64" in command
94
+
95
+ def test_should_not_add_commands_when_no_binaries(self) -> None:
96
+ """Should not add installation commands when plugin has no binaries."""
97
+
98
+ # Arrange
99
+ class NoBinariesPlugin(SpaceforgePlugin):
100
+ pass
101
+
102
+ generator = PluginGenerator()
103
+ generator.plugin_class = NoBinariesPlugin
104
+ generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
105
+ generator.config = {
106
+ "setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
107
+ "plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
108
+ }
109
+
110
+ hooks: Dict[str, List[str]] = {"before_init": []}
111
+
112
+ # Act
113
+ generator._generate_binary_install_command(hooks)
114
+
115
+ # Assert
116
+ assert len(hooks["before_init"]) == 0
117
+
118
+ def test_should_raise_error_when_binary_has_no_download_urls(self) -> None:
119
+ """Should raise ValueError when binary has empty download URLs."""
120
+
121
+ # Arrange
122
+ class InvalidBinaryPlugin(SpaceforgePlugin):
123
+ __binaries__ = [Binary(name="invalid", download_urls={})]
124
+
125
+ generator = PluginGenerator()
126
+ generator.plugin_class = InvalidBinaryPlugin
127
+ generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
128
+ generator.config = {
129
+ "setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
130
+ "plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
131
+ }
132
+
133
+ hooks: Dict[str, List[str]] = {"before_init": []}
134
+
135
+ # Act & Assert
136
+ with pytest.raises(ValueError, match="must have at least one download URL"):
137
+ generator._generate_binary_install_command(hooks)
138
+
139
+ def test_should_handle_single_architecture_binary(self) -> None:
140
+ """Should generate appropriate commands for single architecture binary."""
141
+
142
+ # Arrange
143
+ class SingleArchPlugin(SpaceforgePlugin):
144
+ __binaries__ = [
145
+ Binary(
146
+ name="single-arch",
147
+ download_urls={"amd64": "https://example.com/binary-amd64"},
148
+ )
149
+ ]
150
+
151
+ generator = PluginGenerator()
152
+ generator.plugin_class = SingleArchPlugin
153
+ generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
154
+ generator.config = {
155
+ "setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
156
+ "plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
157
+ }
158
+
159
+ hooks: Dict[str, List[str]] = {"before_init": []}
160
+
161
+ # Act
162
+ generator._generate_binary_install_command(hooks)
163
+ command = hooks["before_init"][-1]
164
+
165
+ # Assert
166
+ assert "https://example.com/binary-amd64" in command
167
+ assert "arm64 binary not available" in command
@@ -0,0 +1,180 @@
1
+ """Tests for PluginGenerator core functionality."""
2
+
3
+ import os
4
+ from unittest.mock import Mock, patch
5
+
6
+ import pytest
7
+
8
+ from spaceforge.generator import PluginGenerator
9
+ from spaceforge.plugin import SpaceforgePlugin
10
+
11
+
12
+ class TestPluginGeneratorInitialization:
13
+ """Test PluginGenerator initialization and configuration."""
14
+
15
+ def test_should_initialize_with_custom_paths(self) -> None:
16
+ """Should accept and store custom plugin and output paths."""
17
+ # Arrange & Act
18
+ generator = PluginGenerator("custom_plugin.py", "custom_output.yaml")
19
+
20
+ # Assert
21
+ assert generator.plugin_path == "custom_plugin.py"
22
+ assert generator.output_path == "custom_output.yaml"
23
+ assert generator.plugin_class is None
24
+ assert generator.plugin_instance is None
25
+ assert generator.plugin_working_directory is None
26
+
27
+ def test_should_use_defaults_when_no_paths_provided(self) -> None:
28
+ """Should use default paths when none specified."""
29
+ # Arrange & Act
30
+ generator = PluginGenerator()
31
+
32
+ # Assert
33
+ assert generator.plugin_path == "plugin.py"
34
+ assert generator.output_path == "plugin.yaml"
35
+
36
+
37
+ class TestPluginGeneratorLoading:
38
+ """Test plugin file loading functionality."""
39
+
40
+ def test_should_raise_file_not_found_when_plugin_file_missing(self) -> None:
41
+ """Should raise FileNotFoundError when plugin file doesn't exist."""
42
+ # Arrange
43
+ generator = PluginGenerator("nonexistent.py")
44
+
45
+ # Act & Assert
46
+ with pytest.raises(FileNotFoundError, match="Plugin file not found"):
47
+ generator.load_plugin()
48
+
49
+ def test_should_raise_exception_when_plugin_file_has_syntax_errors(
50
+ self, temp_dir: str
51
+ ) -> None:
52
+ """Should raise exception when plugin file has invalid Python syntax."""
53
+ # Arrange
54
+ invalid_path = os.path.join(temp_dir, "invalid.py")
55
+ with open(invalid_path, "w") as f:
56
+ f.write("invalid python syntax }")
57
+
58
+ generator = PluginGenerator(invalid_path)
59
+
60
+ # Act & Assert
61
+ with pytest.raises(Exception): # Could be syntax error or import error
62
+ generator.load_plugin()
63
+
64
+ def test_should_raise_value_error_when_no_spaceforge_plugin_found(
65
+ self, temp_dir: str
66
+ ) -> None:
67
+ """Should raise ValueError when file has no SpaceforgePlugin subclass."""
68
+ # Arrange
69
+ no_plugin_path = os.path.join(temp_dir, "no_plugin.py")
70
+ with open(no_plugin_path, "w") as f:
71
+ f.write(
72
+ """
73
+ class NotAPlugin:
74
+ pass
75
+ """
76
+ )
77
+
78
+ generator = PluginGenerator(no_plugin_path)
79
+
80
+ # Act & Assert
81
+ with pytest.raises(ValueError, match="No SpaceforgePlugin subclass found"):
82
+ generator.load_plugin()
83
+
84
+ def test_should_load_plugin_successfully_when_valid_file_provided(
85
+ self, test_plugin_file: str
86
+ ) -> None:
87
+ """Should successfully load plugin from valid file."""
88
+ # Arrange
89
+ generator = PluginGenerator(test_plugin_file)
90
+
91
+ # Act
92
+ generator.load_plugin()
93
+
94
+ # Assert
95
+ assert generator.plugin_class is not None
96
+ assert generator.plugin_instance is not None
97
+ assert generator.plugin_class.__name__ == "TestPlugin"
98
+ assert generator.plugin_working_directory == "/mnt/workspace/plugins/test"
99
+
100
+ @patch("spaceforge.generator.importlib.util.spec_from_file_location")
101
+ def test_should_raise_import_error_when_spec_is_none(
102
+ self, mock_spec: Mock, test_plugin_file: str
103
+ ) -> None:
104
+ """Should raise ImportError when importlib spec creation fails."""
105
+ # Arrange
106
+ mock_spec.return_value = None
107
+ generator = PluginGenerator(test_plugin_file)
108
+
109
+ # Act & Assert
110
+ with pytest.raises(ImportError, match="Could not load plugin"):
111
+ generator.load_plugin()
112
+
113
+
114
+ class TestPluginGeneratorMetadata:
115
+ """Test metadata extraction functionality."""
116
+
117
+ def test_should_extract_complete_metadata_when_all_attributes_present(self) -> None:
118
+ """Should extract all metadata when plugin has complete attributes."""
119
+
120
+ # Arrange
121
+ class CompletePlugin(SpaceforgePlugin):
122
+ """Complete test plugin."""
123
+
124
+ __plugin_name__ = "complete_test"
125
+ __version__ = "2.0.0"
126
+ __author__ = "Test Author"
127
+
128
+ generator = PluginGenerator()
129
+ generator.plugin_class = CompletePlugin
130
+
131
+ # Act
132
+ metadata = generator.get_plugin_metadata()
133
+
134
+ # Assert
135
+ assert metadata["name_prefix"] == "complete_test"
136
+ assert metadata["version"] == "2.0.0"
137
+ assert metadata["author"] == "Test Author"
138
+ assert metadata["description"] == "Complete test plugin."
139
+
140
+ def test_should_use_defaults_when_metadata_attributes_missing(self) -> None:
141
+ """Should use default values when plugin metadata attributes are missing."""
142
+
143
+ # Arrange
144
+ class MinimalPlugin(SpaceforgePlugin):
145
+ pass
146
+
147
+ generator = PluginGenerator()
148
+ generator.plugin_class = MinimalPlugin
149
+
150
+ # Act
151
+ metadata = generator.get_plugin_metadata()
152
+
153
+ # Assert
154
+ assert metadata["name_prefix"] == "SpaceforgePlugin" # inherited
155
+ assert metadata["version"] == "1.0.0" # inherited from base
156
+ assert metadata["author"] == "Spacelift Team" # inherited from base
157
+ assert "MinimalPlugin" in metadata["description"]
158
+
159
+ def test_should_generate_name_from_class_name_when_plugin_name_missing(
160
+ self,
161
+ ) -> None:
162
+ """Should derive name from class name when __plugin_name__ not set."""
163
+
164
+ # Arrange
165
+ class MinimalPlugin: # Don't inherit from SpaceforgePlugin
166
+ __name__ = "MinimalPlugin"
167
+
168
+ generator = PluginGenerator()
169
+ generator.plugin_class = MinimalPlugin # type: ignore[assignment]
170
+
171
+ # Act
172
+ metadata = generator.get_plugin_metadata()
173
+
174
+ # Assert
175
+ assert (
176
+ metadata["name_prefix"] == "minimal"
177
+ ) # class name lowercased with 'plugin' removed
178
+ assert metadata["version"] == "1.0.0" # default
179
+ assert metadata["author"] == "Unknown" # default
180
+ assert "MinimalPlugin" in metadata["description"]
@@ -0,0 +1,90 @@
1
+ """Tests for PluginGenerator hook detection."""
2
+
3
+ import os
4
+
5
+ from pytest import MonkeyPatch
6
+
7
+ from spaceforge.generator import PluginGenerator
8
+ from spaceforge.plugin import SpaceforgePlugin
9
+
10
+
11
+ class TestPluginGeneratorHooks:
12
+ """Test hook method detection functionality."""
13
+
14
+ def test_should_detect_overridden_hook_methods(self) -> None:
15
+ """Should identify hook methods that have been overridden in plugin."""
16
+
17
+ # Arrange
18
+ class HookedPlugin(SpaceforgePlugin):
19
+ def after_plan(self) -> None:
20
+ pass
21
+
22
+ def before_apply(self) -> None:
23
+ pass
24
+
25
+ generator = PluginGenerator()
26
+ generator.plugin_class = HookedPlugin
27
+
28
+ # Act
29
+ hooks = generator.get_available_hooks()
30
+
31
+ # Assert
32
+ assert "after_plan" in hooks
33
+ assert "before_apply" in hooks
34
+ assert len(hooks) == 2
35
+
36
+ def test_should_return_empty_list_when_no_hooks_overridden(self) -> None:
37
+ """Should return empty list when plugin has no overridden hook methods."""
38
+
39
+ # Arrange
40
+ class NoHooksPlugin(SpaceforgePlugin):
41
+ pass
42
+
43
+ generator = PluginGenerator()
44
+ generator.plugin_class = NoHooksPlugin
45
+
46
+ # Act
47
+ hooks = generator.get_available_hooks()
48
+
49
+ # Assert
50
+ assert hooks == []
51
+
52
+ def test_contexts_should_not_have_duplicates_pip_install_in_hooks(
53
+ self, temp_dir: str, monkeypatch: MonkeyPatch
54
+ ) -> None:
55
+ """Should not have duplicate hook methods in the list."""
56
+
57
+ # Arrange
58
+ plugin_content = """
59
+ from spaceforge import SpaceforgePlugin
60
+ class DuplicateHooksPlugin(SpaceforgePlugin):
61
+ def before_init(self) -> None:
62
+ pass
63
+ """
64
+
65
+ requirements_content = """
66
+ requests
67
+ """
68
+
69
+ plugin_path = os.path.join(temp_dir, "plugin.py")
70
+ with open(plugin_path, "w") as f:
71
+ f.write(plugin_content)
72
+ with open(os.path.join(temp_dir, "requirements.txt"), "w") as f:
73
+ f.write(requirements_content)
74
+
75
+ # Change to temporary directory (automatically restored after test)
76
+ monkeypatch.chdir(temp_dir)
77
+
78
+ generator = PluginGenerator(plugin_path)
79
+ generator.load_plugin()
80
+
81
+ # Act
82
+ contexts = generator.get_plugin_contexts()
83
+ hooks = contexts[0].hooks
84
+ assert hooks is not None
85
+
86
+ # Assert
87
+ processed_hooks = []
88
+ for hook in hooks["before_init"]:
89
+ assert hook not in processed_hooks
90
+ processed_hooks.append(hook)