julee 0.1.4__py3-none-any.whl → 0.1.6__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 (165) hide show
  1. julee/__init__.py +1 -1
  2. julee/api/tests/routers/test_assembly_specifications.py +2 -0
  3. julee/api/tests/routers/test_documents.py +2 -0
  4. julee/api/tests/routers/test_knowledge_service_configs.py +2 -0
  5. julee/api/tests/routers/test_knowledge_service_queries.py +2 -0
  6. julee/api/tests/routers/test_system.py +2 -0
  7. julee/api/tests/routers/test_workflows.py +2 -0
  8. julee/api/tests/test_app.py +2 -0
  9. julee/api/tests/test_dependencies.py +2 -0
  10. julee/api/tests/test_requests.py +2 -0
  11. julee/contrib/polling/__init__.py +22 -19
  12. julee/contrib/polling/apps/__init__.py +17 -0
  13. julee/contrib/polling/apps/worker/__init__.py +17 -0
  14. julee/contrib/polling/apps/worker/pipelines.py +288 -0
  15. julee/contrib/polling/domain/__init__.py +7 -9
  16. julee/contrib/polling/domain/models/__init__.py +6 -7
  17. julee/contrib/polling/domain/models/polling_config.py +18 -1
  18. julee/contrib/polling/domain/services/__init__.py +6 -5
  19. julee/contrib/polling/domain/services/poller.py +1 -1
  20. julee/contrib/polling/infrastructure/__init__.py +9 -8
  21. julee/contrib/polling/infrastructure/services/__init__.py +6 -5
  22. julee/contrib/polling/infrastructure/services/polling/__init__.py +6 -5
  23. julee/contrib/polling/infrastructure/services/polling/http/__init__.py +6 -5
  24. julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +5 -2
  25. julee/contrib/polling/infrastructure/temporal/__init__.py +12 -12
  26. julee/contrib/polling/infrastructure/temporal/activities.py +1 -1
  27. julee/contrib/polling/infrastructure/temporal/manager.py +291 -0
  28. julee/contrib/polling/infrastructure/temporal/proxies.py +1 -1
  29. julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +580 -0
  30. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +40 -2
  31. julee/contrib/polling/tests/unit/infrastructure/temporal/__init__.py +7 -0
  32. julee/contrib/polling/tests/unit/infrastructure/temporal/test_manager.py +475 -0
  33. julee/docs/sphinx_hcd/__init__.py +146 -13
  34. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  35. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  36. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  37. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  38. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  39. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  40. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  41. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  42. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  43. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  44. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  45. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  46. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  47. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  48. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  49. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  50. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  51. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  52. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  53. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  54. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  55. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  56. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  57. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  58. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  59. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  60. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  61. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  62. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  63. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  64. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  65. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  66. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  67. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  68. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  69. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  70. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  71. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  72. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  73. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  74. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  75. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  76. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  77. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  78. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  79. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  80. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  81. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  82. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  83. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  84. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  85. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  86. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  87. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  88. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  89. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  90. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  91. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  92. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  93. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  94. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  95. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  96. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  97. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  98. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  99. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  100. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  101. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  102. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  103. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  104. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  105. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  106. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  107. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  108. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  109. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  110. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  111. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  112. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  113. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  114. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  115. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  116. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  117. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  118. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  119. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  120. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  121. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  122. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  123. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  124. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  125. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  126. julee/domain/models/assembly/tests/test_assembly.py +2 -0
  127. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +2 -0
  128. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +2 -0
  129. julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -0
  130. julee/domain/models/document/tests/test_document.py +2 -0
  131. julee/domain/models/policy/tests/test_document_policy_validation.py +2 -0
  132. julee/domain/models/policy/tests/test_policy.py +2 -0
  133. julee/domain/use_cases/tests/test_extract_assemble_data.py +2 -0
  134. julee/domain/use_cases/tests/test_initialize_system_data.py +2 -0
  135. julee/domain/use_cases/tests/test_validate_document.py +2 -0
  136. julee/maintenance/release.py +10 -5
  137. julee/repositories/memory/tests/test_document.py +2 -0
  138. julee/repositories/memory/tests/test_document_policy_validation.py +2 -0
  139. julee/repositories/memory/tests/test_policy.py +2 -0
  140. julee/repositories/minio/tests/test_assembly.py +2 -0
  141. julee/repositories/minio/tests/test_assembly_specification.py +2 -0
  142. julee/repositories/minio/tests/test_client_protocol.py +3 -0
  143. julee/repositories/minio/tests/test_document.py +2 -0
  144. julee/repositories/minio/tests/test_document_policy_validation.py +2 -0
  145. julee/repositories/minio/tests/test_knowledge_service_config.py +2 -0
  146. julee/repositories/minio/tests/test_knowledge_service_query.py +2 -0
  147. julee/repositories/minio/tests/test_policy.py +2 -0
  148. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +2 -0
  149. julee/services/knowledge_service/memory/test_knowledge_service.py +2 -0
  150. julee/services/knowledge_service/test_factory.py +2 -0
  151. julee/util/tests/test_decorators.py +2 -0
  152. julee-0.1.6.dist-info/METADATA +104 -0
  153. julee-0.1.6.dist-info/RECORD +288 -0
  154. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  155. julee/docs/sphinx_hcd/apps.py +0 -518
  156. julee/docs/sphinx_hcd/epics.py +0 -453
  157. julee/docs/sphinx_hcd/integrations.py +0 -310
  158. julee/docs/sphinx_hcd/journeys.py +0 -797
  159. julee/docs/sphinx_hcd/personas.py +0 -457
  160. julee/docs/sphinx_hcd/stories.py +0 -960
  161. julee-0.1.4.dist-info/METADATA +0 -197
  162. julee-0.1.4.dist-info/RECORD +0 -196
  163. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
  164. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
  165. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,496 @@
1
+ """Tests for YAML manifest parser."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from julee.docs.sphinx_hcd.domain.models.app import AppType
8
+ from julee.docs.sphinx_hcd.domain.models.integration import Direction
9
+ from julee.docs.sphinx_hcd.parsers.yaml import (
10
+ parse_app_manifest,
11
+ parse_integration_manifest,
12
+ parse_manifest_content,
13
+ scan_app_manifests,
14
+ scan_integration_manifests,
15
+ )
16
+
17
+
18
+ class TestParseManifestContent:
19
+ """Test parse_manifest_content function."""
20
+
21
+ def test_parse_valid_yaml(self) -> None:
22
+ """Test parsing valid YAML content."""
23
+ content = """
24
+ name: Staff Portal
25
+ type: staff
26
+ status: live
27
+ description: Portal for staff members
28
+ accelerators:
29
+ - user-auth
30
+ - doc-upload
31
+ """
32
+ result = parse_manifest_content(content)
33
+ assert result is not None
34
+ assert result["name"] == "Staff Portal"
35
+ assert result["type"] == "staff"
36
+ assert result["status"] == "live"
37
+ assert result["accelerators"] == ["user-auth", "doc-upload"]
38
+
39
+ def test_parse_empty_content(self) -> None:
40
+ """Test parsing empty content."""
41
+ result = parse_manifest_content("")
42
+ assert result is None
43
+
44
+ def test_parse_invalid_yaml(self) -> None:
45
+ """Test parsing invalid YAML."""
46
+ content = """
47
+ name: Test
48
+ invalid yaml: [unclosed bracket
49
+ """
50
+ result = parse_manifest_content(content)
51
+ assert result is None
52
+
53
+ def test_parse_minimal_yaml(self) -> None:
54
+ """Test parsing minimal YAML."""
55
+ content = "name: Test App"
56
+ result = parse_manifest_content(content)
57
+ assert result is not None
58
+ assert result["name"] == "Test App"
59
+
60
+
61
+ class TestParseAppManifest:
62
+ """Test parse_app_manifest function."""
63
+
64
+ @pytest.fixture
65
+ def temp_project(self, tmp_path: Path) -> Path:
66
+ """Create a temporary project structure."""
67
+ apps_dir = tmp_path / "apps"
68
+ apps_dir.mkdir()
69
+ return tmp_path
70
+
71
+ def test_parse_complete_manifest(self, temp_project: Path) -> None:
72
+ """Test parsing a complete app manifest."""
73
+ app_dir = temp_project / "apps" / "staff-portal"
74
+ app_dir.mkdir(parents=True)
75
+ manifest = app_dir / "app.yaml"
76
+ manifest.write_text(
77
+ """
78
+ name: Staff Portal
79
+ type: staff
80
+ status: live
81
+ description: Portal for staff members
82
+ accelerators:
83
+ - user-auth
84
+ """
85
+ )
86
+
87
+ app = parse_app_manifest(manifest)
88
+
89
+ assert app is not None
90
+ assert app.slug == "staff-portal"
91
+ assert app.name == "Staff Portal"
92
+ assert app.app_type == AppType.STAFF
93
+ assert app.status == "live"
94
+ assert app.accelerators == ["user-auth"]
95
+
96
+ def test_parse_manifest_with_explicit_slug(self, temp_project: Path) -> None:
97
+ """Test parsing with explicit app slug override."""
98
+ app_dir = temp_project / "apps" / "original-slug"
99
+ app_dir.mkdir(parents=True)
100
+ manifest = app_dir / "app.yaml"
101
+ manifest.write_text("name: Test App")
102
+
103
+ app = parse_app_manifest(manifest, app_slug="override-slug")
104
+
105
+ assert app is not None
106
+ assert app.slug == "override-slug"
107
+
108
+ def test_parse_manifest_default_name(self, temp_project: Path) -> None:
109
+ """Test default name generated from slug."""
110
+ app_dir = temp_project / "apps" / "my-cool-app"
111
+ app_dir.mkdir(parents=True)
112
+ manifest = app_dir / "app.yaml"
113
+ manifest.write_text("type: staff")
114
+
115
+ app = parse_app_manifest(manifest)
116
+
117
+ assert app is not None
118
+ assert app.name == "My Cool App"
119
+
120
+ def test_parse_manifest_nonexistent(self, temp_project: Path) -> None:
121
+ """Test parsing a nonexistent file returns None."""
122
+ nonexistent = temp_project / "apps" / "nonexistent" / "app.yaml"
123
+ app = parse_app_manifest(nonexistent)
124
+ assert app is None
125
+
126
+ def test_parse_manifest_empty_file(self, temp_project: Path) -> None:
127
+ """Test parsing an empty manifest file."""
128
+ app_dir = temp_project / "apps" / "empty-app"
129
+ app_dir.mkdir(parents=True)
130
+ manifest = app_dir / "app.yaml"
131
+ manifest.write_text("")
132
+
133
+ app = parse_app_manifest(manifest)
134
+ assert app is None
135
+
136
+ def test_parse_manifest_invalid_yaml(self, temp_project: Path) -> None:
137
+ """Test parsing invalid YAML returns None."""
138
+ app_dir = temp_project / "apps" / "bad-app"
139
+ app_dir.mkdir(parents=True)
140
+ manifest = app_dir / "app.yaml"
141
+ manifest.write_text("invalid: [unclosed")
142
+
143
+ app = parse_app_manifest(manifest)
144
+ assert app is None
145
+
146
+
147
+ class TestScanAppManifests:
148
+ """Test scan_app_manifests function."""
149
+
150
+ @pytest.fixture
151
+ def temp_project(self, tmp_path: Path) -> Path:
152
+ """Create a temporary project with multiple apps."""
153
+ apps_dir = tmp_path / "apps"
154
+ apps_dir.mkdir()
155
+
156
+ # Create app1
157
+ app1_dir = apps_dir / "staff-portal"
158
+ app1_dir.mkdir()
159
+ (app1_dir / "app.yaml").write_text(
160
+ """
161
+ name: Staff Portal
162
+ type: staff
163
+ """
164
+ )
165
+
166
+ # Create app2
167
+ app2_dir = apps_dir / "customer-portal"
168
+ app2_dir.mkdir()
169
+ (app2_dir / "app.yaml").write_text(
170
+ """
171
+ name: Customer Portal
172
+ type: external
173
+ """
174
+ )
175
+
176
+ # Create app3 (member tool)
177
+ app3_dir = apps_dir / "member-tool"
178
+ app3_dir.mkdir()
179
+ (app3_dir / "app.yaml").write_text(
180
+ """
181
+ name: Member Tool
182
+ type: member-tool
183
+ """
184
+ )
185
+
186
+ return tmp_path
187
+
188
+ def test_scan_finds_all_apps(self, temp_project: Path) -> None:
189
+ """Test scanning finds all app manifests."""
190
+ apps_dir = temp_project / "apps"
191
+ apps = scan_app_manifests(apps_dir)
192
+
193
+ assert len(apps) == 3
194
+ slugs = {a.slug for a in apps}
195
+ assert slugs == {"staff-portal", "customer-portal", "member-tool"}
196
+
197
+ def test_scan_extracts_types(self, temp_project: Path) -> None:
198
+ """Test scanning correctly extracts app types."""
199
+ apps_dir = temp_project / "apps"
200
+ apps = scan_app_manifests(apps_dir)
201
+
202
+ types_by_slug = {a.slug: a.app_type for a in apps}
203
+ assert types_by_slug["staff-portal"] == AppType.STAFF
204
+ assert types_by_slug["customer-portal"] == AppType.EXTERNAL
205
+ assert types_by_slug["member-tool"] == AppType.MEMBER_TOOL
206
+
207
+ def test_scan_nonexistent_directory(self, tmp_path: Path) -> None:
208
+ """Test scanning nonexistent directory returns empty list."""
209
+ apps = scan_app_manifests(tmp_path / "nonexistent")
210
+ assert apps == []
211
+
212
+ def test_scan_empty_directory(self, tmp_path: Path) -> None:
213
+ """Test scanning empty directory returns empty list."""
214
+ empty_dir = tmp_path / "empty"
215
+ empty_dir.mkdir()
216
+ apps = scan_app_manifests(empty_dir)
217
+ assert apps == []
218
+
219
+ def test_scan_ignores_files_in_root(self, tmp_path: Path) -> None:
220
+ """Test scanning ignores non-directory items."""
221
+ apps_dir = tmp_path / "apps"
222
+ apps_dir.mkdir()
223
+
224
+ # Create a file in the apps dir (should be ignored)
225
+ (apps_dir / "README.md").write_text("readme")
226
+
227
+ # Create a valid app
228
+ app_dir = apps_dir / "test-app"
229
+ app_dir.mkdir()
230
+ (app_dir / "app.yaml").write_text("name: Test App")
231
+
232
+ apps = scan_app_manifests(apps_dir)
233
+ assert len(apps) == 1
234
+ assert apps[0].slug == "test-app"
235
+
236
+ def test_scan_skips_directories_without_manifest(self, tmp_path: Path) -> None:
237
+ """Test scanning skips directories without app.yaml."""
238
+ apps_dir = tmp_path / "apps"
239
+ apps_dir.mkdir()
240
+
241
+ # Create directory without manifest
242
+ (apps_dir / "no-manifest").mkdir()
243
+
244
+ # Create valid app
245
+ app_dir = apps_dir / "valid-app"
246
+ app_dir.mkdir()
247
+ (app_dir / "app.yaml").write_text("name: Valid App")
248
+
249
+ apps = scan_app_manifests(apps_dir)
250
+ assert len(apps) == 1
251
+ assert apps[0].slug == "valid-app"
252
+
253
+
254
+ # Integration manifest parsing tests
255
+
256
+
257
+ class TestParseIntegrationManifest:
258
+ """Test parse_integration_manifest function."""
259
+
260
+ @pytest.fixture
261
+ def temp_project(self, tmp_path: Path) -> Path:
262
+ """Create a temporary project structure."""
263
+ integrations_dir = tmp_path / "integrations"
264
+ integrations_dir.mkdir()
265
+ return tmp_path
266
+
267
+ def test_parse_complete_manifest(self, temp_project: Path) -> None:
268
+ """Test parsing a complete integration manifest."""
269
+ int_dir = temp_project / "integrations" / "pilot_data_collection"
270
+ int_dir.mkdir(parents=True)
271
+ manifest = int_dir / "integration.yaml"
272
+ manifest.write_text(
273
+ """
274
+ slug: pilot-data
275
+ name: Pilot Data Collection
276
+ description: Collects pilot data from external systems
277
+ direction: inbound
278
+ depends_on:
279
+ - name: Pilot API
280
+ url: https://pilot.example.com
281
+ - name: Data Lake
282
+ """
283
+ )
284
+
285
+ integration = parse_integration_manifest(manifest)
286
+
287
+ assert integration is not None
288
+ assert integration.slug == "pilot-data"
289
+ assert integration.module == "pilot_data_collection"
290
+ assert integration.name == "Pilot Data Collection"
291
+ assert integration.direction == Direction.INBOUND
292
+ assert len(integration.depends_on) == 2
293
+ assert integration.depends_on[0].name == "Pilot API"
294
+ assert integration.depends_on[0].url == "https://pilot.example.com"
295
+
296
+ def test_parse_manifest_with_explicit_module(self, temp_project: Path) -> None:
297
+ """Test parsing with explicit module name override."""
298
+ int_dir = temp_project / "integrations" / "original_module"
299
+ int_dir.mkdir(parents=True)
300
+ manifest = int_dir / "integration.yaml"
301
+ manifest.write_text("name: Test Integration")
302
+
303
+ integration = parse_integration_manifest(
304
+ manifest, module_name="override_module"
305
+ )
306
+
307
+ assert integration is not None
308
+ assert integration.module == "override_module"
309
+
310
+ def test_parse_manifest_default_slug(self, temp_project: Path) -> None:
311
+ """Test default slug from module name."""
312
+ int_dir = temp_project / "integrations" / "my_integration"
313
+ int_dir.mkdir(parents=True)
314
+ manifest = int_dir / "integration.yaml"
315
+ manifest.write_text("name: My Integration")
316
+
317
+ integration = parse_integration_manifest(manifest)
318
+
319
+ assert integration is not None
320
+ assert integration.slug == "my-integration"
321
+
322
+ def test_parse_manifest_default_name(self, temp_project: Path) -> None:
323
+ """Test default name from slug."""
324
+ int_dir = temp_project / "integrations" / "data_sync"
325
+ int_dir.mkdir(parents=True)
326
+ manifest = int_dir / "integration.yaml"
327
+ manifest.write_text("direction: outbound")
328
+
329
+ integration = parse_integration_manifest(manifest)
330
+
331
+ assert integration is not None
332
+ assert integration.name == "Data Sync"
333
+
334
+ def test_parse_manifest_default_direction(self, temp_project: Path) -> None:
335
+ """Test default direction is bidirectional."""
336
+ int_dir = temp_project / "integrations" / "test"
337
+ int_dir.mkdir(parents=True)
338
+ manifest = int_dir / "integration.yaml"
339
+ manifest.write_text("name: Test")
340
+
341
+ integration = parse_integration_manifest(manifest)
342
+
343
+ assert integration is not None
344
+ assert integration.direction == Direction.BIDIRECTIONAL
345
+
346
+ def test_parse_manifest_nonexistent(self, temp_project: Path) -> None:
347
+ """Test parsing a nonexistent file returns None."""
348
+ nonexistent = temp_project / "integrations" / "nonexistent" / "integration.yaml"
349
+ integration = parse_integration_manifest(nonexistent)
350
+ assert integration is None
351
+
352
+ def test_parse_manifest_empty_file(self, temp_project: Path) -> None:
353
+ """Test parsing an empty manifest file."""
354
+ int_dir = temp_project / "integrations" / "empty"
355
+ int_dir.mkdir(parents=True)
356
+ manifest = int_dir / "integration.yaml"
357
+ manifest.write_text("")
358
+
359
+ integration = parse_integration_manifest(manifest)
360
+ assert integration is None
361
+
362
+ def test_parse_manifest_invalid_yaml(self, temp_project: Path) -> None:
363
+ """Test parsing invalid YAML returns None."""
364
+ int_dir = temp_project / "integrations" / "bad"
365
+ int_dir.mkdir(parents=True)
366
+ manifest = int_dir / "integration.yaml"
367
+ manifest.write_text("invalid: [unclosed")
368
+
369
+ integration = parse_integration_manifest(manifest)
370
+ assert integration is None
371
+
372
+
373
+ class TestScanIntegrationManifests:
374
+ """Test scan_integration_manifests function."""
375
+
376
+ @pytest.fixture
377
+ def temp_project(self, tmp_path: Path) -> Path:
378
+ """Create a temporary project with multiple integrations."""
379
+ integrations_dir = tmp_path / "integrations"
380
+ integrations_dir.mkdir()
381
+
382
+ # Create inbound integration
383
+ int1_dir = integrations_dir / "pilot_data"
384
+ int1_dir.mkdir()
385
+ (int1_dir / "integration.yaml").write_text(
386
+ """
387
+ name: Pilot Data
388
+ direction: inbound
389
+ """
390
+ )
391
+
392
+ # Create outbound integration
393
+ int2_dir = integrations_dir / "analytics_export"
394
+ int2_dir.mkdir()
395
+ (int2_dir / "integration.yaml").write_text(
396
+ """
397
+ name: Analytics Export
398
+ direction: outbound
399
+ """
400
+ )
401
+
402
+ # Create bidirectional integration
403
+ int3_dir = integrations_dir / "data_sync"
404
+ int3_dir.mkdir()
405
+ (int3_dir / "integration.yaml").write_text(
406
+ """
407
+ name: Data Sync
408
+ direction: bidirectional
409
+ """
410
+ )
411
+
412
+ return tmp_path
413
+
414
+ def test_scan_finds_all_integrations(self, temp_project: Path) -> None:
415
+ """Test scanning finds all integration manifests."""
416
+ integrations_dir = temp_project / "integrations"
417
+ integrations = scan_integration_manifests(integrations_dir)
418
+
419
+ assert len(integrations) == 3
420
+ slugs = {i.slug for i in integrations}
421
+ assert slugs == {"pilot-data", "analytics-export", "data-sync"}
422
+
423
+ def test_scan_extracts_directions(self, temp_project: Path) -> None:
424
+ """Test scanning correctly extracts directions."""
425
+ integrations_dir = temp_project / "integrations"
426
+ integrations = scan_integration_manifests(integrations_dir)
427
+
428
+ directions_by_slug = {i.slug: i.direction for i in integrations}
429
+ assert directions_by_slug["pilot-data"] == Direction.INBOUND
430
+ assert directions_by_slug["analytics-export"] == Direction.OUTBOUND
431
+ assert directions_by_slug["data-sync"] == Direction.BIDIRECTIONAL
432
+
433
+ def test_scan_nonexistent_directory(self, tmp_path: Path) -> None:
434
+ """Test scanning nonexistent directory returns empty list."""
435
+ integrations = scan_integration_manifests(tmp_path / "nonexistent")
436
+ assert integrations == []
437
+
438
+ def test_scan_empty_directory(self, tmp_path: Path) -> None:
439
+ """Test scanning empty directory returns empty list."""
440
+ empty_dir = tmp_path / "empty"
441
+ empty_dir.mkdir()
442
+ integrations = scan_integration_manifests(empty_dir)
443
+ assert integrations == []
444
+
445
+ def test_scan_ignores_underscore_directories(self, tmp_path: Path) -> None:
446
+ """Test scanning ignores directories starting with underscore."""
447
+ integrations_dir = tmp_path / "integrations"
448
+ integrations_dir.mkdir()
449
+
450
+ # Create ignored directory
451
+ ignored_dir = integrations_dir / "_base"
452
+ ignored_dir.mkdir()
453
+ (ignored_dir / "integration.yaml").write_text("name: Base")
454
+
455
+ # Create valid integration
456
+ int_dir = integrations_dir / "valid"
457
+ int_dir.mkdir()
458
+ (int_dir / "integration.yaml").write_text("name: Valid")
459
+
460
+ integrations = scan_integration_manifests(integrations_dir)
461
+ assert len(integrations) == 1
462
+ assert integrations[0].slug == "valid"
463
+
464
+ def test_scan_ignores_files_in_root(self, tmp_path: Path) -> None:
465
+ """Test scanning ignores non-directory items."""
466
+ integrations_dir = tmp_path / "integrations"
467
+ integrations_dir.mkdir()
468
+
469
+ # Create a file in the integrations dir (should be ignored)
470
+ (integrations_dir / "README.md").write_text("readme")
471
+
472
+ # Create valid integration
473
+ int_dir = integrations_dir / "test"
474
+ int_dir.mkdir()
475
+ (int_dir / "integration.yaml").write_text("name: Test")
476
+
477
+ integrations = scan_integration_manifests(integrations_dir)
478
+ assert len(integrations) == 1
479
+ assert integrations[0].slug == "test"
480
+
481
+ def test_scan_skips_directories_without_manifest(self, tmp_path: Path) -> None:
482
+ """Test scanning skips directories without integration.yaml."""
483
+ integrations_dir = tmp_path / "integrations"
484
+ integrations_dir.mkdir()
485
+
486
+ # Create directory without manifest
487
+ (integrations_dir / "no_manifest").mkdir()
488
+
489
+ # Create valid integration
490
+ int_dir = integrations_dir / "valid"
491
+ int_dir.mkdir()
492
+ (int_dir / "integration.yaml").write_text("name: Valid")
493
+
494
+ integrations = scan_integration_manifests(integrations_dir)
495
+ assert len(integrations) == 1
496
+ assert integrations[0].slug == "valid"
@@ -0,0 +1 @@
1
+ """Repository implementation tests."""